Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/FwHeadless/FwHeadlessKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public static IServiceCollection AddFwHeadless(this IServiceCollection services)
.BindConfiguration("FwHeadlessConfig")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<MaintenanceModeConfig>()
.BindConfiguration("MaintenanceMode")
.ValidateDataAnnotations()
.ValidateOnStart();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
services.AddSingleton<ISyncJobStatusService, SyncJobStatusService>();
services.AddScoped<ISendReceiveService, SendReceiveService>();
services.AddScoped<IProjectLookupService, ProjectLookupService>();
Expand Down
7 changes: 7 additions & 0 deletions backend/FwHeadless/MaintenanceModeConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace FwHeadless;

public class MaintenanceModeConfig
{
public bool ReadOnlyMode { get; init; } = false;
public string? MaintenanceMessage { get; set; } = null;
}
49 changes: 49 additions & 0 deletions backend/FwHeadless/MaintenanceModeMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.Extensions.Options;

namespace FwHeadless;

public class MaintenanceModeMiddleware(RequestDelegate next, IOptions<MaintenanceModeConfig> config)
{
public const string DefaultMaintenanceMessage = "Lexbox is in read-only mode for scheduled maintenance, please try again in an hour or two";

#pragma warning disable IDE0022
public static bool IsWriteMethod(string httpMethod) => httpMethod switch
{
// Sadly, readonly static vars do not count as consts for switch expressions
var m when m == HttpMethods.Post => true,
var m when m == HttpMethods.Put => true,
var m when m == HttpMethods.Patch => true,
var m when m == HttpMethods.Delete => true,
_ => false
};
#pragma warning restore IDE0022

public async Task Invoke(HttpContext context)
{
var readOnly = config.Value.ReadOnlyMode;
var message = config.Value.MaintenanceMessage;
// If read-only mode is set without an explicit message, use the default
if (readOnly) message ??= DefaultMaintenanceMessage;
// But if not in read-only mode, then an empty message should not set the header

// If we get here without a maintenance message, exit fast
if (string.IsNullOrEmpty(message))
{
await next(context);
return;
}

// Non-empty maintenance messages should be set on all requests
context.Response.Headers["maintenance-message"] = message;

// But request filtering should only happen if we're in read-only mode
if (readOnly && IsWriteMethod(context.Request.Method))
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync(message);
return;
}

await next(context);
}
}
3 changes: 3 additions & 0 deletions backend/FwHeadless/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
await next();
});

// Allow read-only mode during maintenance windows
app.UseMiddleware<MaintenanceModeMiddleware>();

// Load project ID from request
app.Use((context, next) =>
{
Expand Down
4 changes: 4 additions & 0 deletions backend/FwHeadless/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"Default": "Information"
}
},
"MaintenanceMode": {
"ReadOnlyMode": false,
"MaintenanceMessage": null
},
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"
}
4 changes: 4 additions & 0 deletions backend/FwHeadless/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"SendReceive": "Information"
}
},
"MaintenanceMode": {
"ReadOnlyMode": false,
"MaintenanceMessage": null
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"LcmCrdt": {
// This value is explicitly referenced in SnapshotAtCommitService when preserving FieldWorks commits
"DefaultAuthorForCommits": "FieldWorks",
Expand Down
7 changes: 7 additions & 0 deletions backend/LexBoxApi/Config/MaintenanceModeConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace LexBoxApi.Config;

public class MaintenanceModeConfig
{
public bool ReadOnlyMode { get; init; } = false;
public string? MaintenanceMessage { get; set; } = null;
}
4 changes: 4 additions & 0 deletions backend/LexBoxApi/LexBoxKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public static void AddLexBoxApi(this IServiceCollection services,
.BindConfiguration("FwLiteRelease")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<MaintenanceModeConfig>()
.BindConfiguration("MaintenanceMode")
.ValidateDataAnnotations()
.ValidateOnStart();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
services.AddHttpClient();
services.AddServiceDiscovery();
services.AddHttpClient<FwHeadlessClient>(client => client.BaseAddress = new ("http://fwHeadless"))
Expand Down
50 changes: 50 additions & 0 deletions backend/LexBoxApi/Middleware/MaintenanceModeMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using LexBoxApi.Config;
using Microsoft.Extensions.Options;

namespace LexBoxApi.Middleware;

public class MaintenanceModeMiddleware(RequestDelegate next, IOptions<MaintenanceModeConfig> config)
{
public const string DefaultMaintenanceMessage = "Lexbox is in read-only mode for scheduled maintenance, please try again in an hour or two";

#pragma warning disable IDE0022
public static bool IsWriteMethod(string httpMethod) => httpMethod switch
{
// Sadly, readonly static vars do not count as consts for switch expressions
var m when m == HttpMethods.Post => true,
var m when m == HttpMethods.Put => true,
var m when m == HttpMethods.Patch => true,
var m when m == HttpMethods.Delete => true,
_ => false
};
#pragma warning restore IDE0022

public async Task Invoke(HttpContext context)
{
var readOnly = config.Value.ReadOnlyMode;
var message = config.Value.MaintenanceMessage;
// If read-only mode is set without an explicit message, use the default
if (readOnly) message ??= DefaultMaintenanceMessage;
// But if not in read-only mode, then an empty message should not set the header

// If we get here without a maintenance message, exit fast
if (string.IsNullOrEmpty(message))
{
await next(context);
return;
}

// Non-empty maintenance messages should be set on all requests
context.Response.Headers["maintenance-message"] = message;

// But request filtering should only happen if we're in read-only mode
if (readOnly && IsWriteMethod(context.Request.Method))
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync(message);
return;
}

await next(context);
}
}
2 changes: 2 additions & 0 deletions backend/LexBoxApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using LexBoxApi.Auth.Attributes;
using LexBoxApi.ErrorHandling;
using LexBoxApi.Hub;
using LexBoxApi.Middleware;
using LexBoxApi.Otel;
using LexBoxApi.Proxies;
using LexBoxApi.Services;
Expand Down Expand Up @@ -157,6 +158,7 @@
if (!app.Environment.IsDevelopment())
app.UseExceptionHandler();
app.UseHealthChecks("/api/healthz");
app.UseMiddleware<MaintenanceModeMiddleware>();
// Configure the HTTP request pipeline.
//for now allow this to run in prod, maybe later we want to disable it.
{
Expand Down
4 changes: 4 additions & 0 deletions backend/LexBoxApi/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
"LfMergeTrustToken": "lf-merge-dev-trust-token",
"AutoUpdateLexEntryCountOnSendReceive": true
},
"MaintenanceMode": {
"ReadOnlyMode": false,
"MaintenanceMessage": null
},
"HealthChecks": {
"RequireFwHeadlessContainerVersionMatch": false,
"RequireHealthyFwHeadlessContainer": false
Expand Down
4 changes: 4 additions & 0 deletions backend/LexBoxApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"HgConfig": {
"RepoPath": null
},
"MaintenanceMode": {
"ReadOnlyMode": false,
"MaintenanceMessage": null
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"Authentication": {
"Jwt": {
"Secret": null,
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ensureErrorIsTraced, traceFetch } from '$lib/otel/otel.client';
import { getErrorMessage, validateFetchResponse } from './hooks.shared';

import { maintenanceMessage } from '$lib/util/maintenance';
import { APP_VERSION } from '$lib/util/version';
import type { HandleClientError } from '@sveltejs/kit';
import { USER_LOAD_KEY } from '$lib/user';
Expand Down Expand Up @@ -68,6 +69,12 @@
return response;
});

if (response.headers.has('maintenance-message')) {
maintenanceMessage.value = response.headers.get('maintenance-message');

Check failure on line 73 in frontend/src/hooks.client.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/hooks.client.ts#L73

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .value on a type that cannot be resolved.
} else {
maintenanceMessage.value = null;

Check failure on line 75 in frontend/src/hooks.client.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/hooks.client.ts#L75

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .value on a type that cannot be resolved.
}

// invalidateUserOnJwtRefresh is considered true by default, so only skip if the value is false
if (args[1]?.lexboxResponseHandlingConfig?.invalidateUserOnJwtRefresh !== false && response.headers.get('lexbox-jwt-updated') === 'all') {
await invalidate(USER_LOAD_KEY);
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {loadI18n, pickBestLocale} from '$lib/i18n';
import {AUTH_COOKIE_NAME, getUser, isAuthn} from '$lib/user';
import {maintenanceMessage} from '$lib/util/maintenance';
import {apiVersion} from '$lib/util/version';
import {redirect, type Handle, type HandleFetch, type HandleServerError, type RequestEvent, type ResolveOptions} from '@sveltejs/kit';
import {ensureErrorIsTraced, traceRequest, traceFetch} from '$lib/otel/otel.server';
Expand Down Expand Up @@ -88,6 +89,12 @@
apiVersion.value = response.headers.get('lexbox-version');
}

if (response.headers.has('maintenance-message')) {
maintenanceMessage.value = response.headers.get('maintenance-message');

Check failure on line 93 in frontend/src/hooks.server.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/hooks.server.ts#L93

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .value on a type that cannot be resolved.
} else {
maintenanceMessage.value = null;

Check failure on line 95 in frontend/src/hooks.server.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/hooks.server.ts#L95

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .value on a type that cannot be resolved.
}

const lexBoxSetAuthCookieHeader = response.headers.getSetCookie()
.find(h => h.startsWith(`${AUTH_COOKIE_NAME}=`));

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/lib/layout/AppBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {AuthenticatedUserIcon, UserAddOutline, Icon} from '$lib/icons';
import {onMount} from 'svelte';
import type {LexAuthUser} from '$lib/user';
import {maintenanceMessage} from '$lib/util/maintenance';
import {page} from '$app/state';
import AppLogo from '$lib/icons/AppLogo.svelte';
import DevContent from './DevContent.svelte';
Expand All @@ -25,6 +26,11 @@

<!-- https://daisyui.com/components/navbar -->
<header>
{#if maintenanceMessage.value}
<span class="flex gap-2 justify-center items-center bg-warning text-warning-content p-2">
{maintenanceMessage.value}
</span>
{/if}
{#if environmentName !== 'production'}
<a href="https://lexbox.org" class="flex gap-2 justify-center items-center bg-warning text-warning-content p-2 underline">
{$t('environment_warning', { environmentName })}
Expand Down
Loading