Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ To run the host, `dotnet run --project src/FluentCMS.Host` starts the web host o

On a fresh database the admin asks for the first administrator at `/setup`; every other admin page needs a signed-in user. Data Protection keeps the keys that protect sign-in cookies, anti-forgery tokens and stored secrets in `DataProtection__KeysDirectory` (default `data-protection-keys` under the content root). Put it on storage that survives restarts and upgrades and that only the host's account can read; losing it signs everyone out. Sign-in and setup posts are limited per client address by `SignInRateLimit__PermitLimit` and `SignInRateLimit__Window`, ten a minute by default.

Signed in, `/ui` shows every component in `src/FluentCMS.Admin/Components/Ui` with its keyboard hints. Switch the theme from the header and the direction with the link on the page. A new component goes on that page in the same pull request; a test fails when one is missing.

People can also sign in through one OpenID Connect provider (Keycloak, Authentik, Microsoft Entra, Google and similar). Set `Authentication__OpenIdConnect__Authority`, `Authentication__OpenIdConnect__ClientId` and `Authentication__OpenIdConnect__ClientSecret`, optionally `Authentication__OpenIdConnect__DisplayName` for the button, and register `https://<host>/signin-oidc` as the redirect URI at the provider. Without an authority and client id the option does not appear. Users are not created from the provider: the first sign-in links the provider's account to the user with the same email, and only if the provider marks the email as verified. Later sign-ins match on the provider's issuer and subject. A provider that never sends `email_verified`, such as Microsoft Entra, needs `Authentication__OpenIdConnect__AssumeVerifiedEmail=true`; set it only if that provider issues verified addresses alone.

## Database migrations
Expand Down
7 changes: 0 additions & 7 deletions src/FluentCMS.Admin/AdminOptions.cs

This file was deleted.

7 changes: 2 additions & 5 deletions src/FluentCMS.Admin/AdminServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ namespace FluentCMS.Admin;

public static class AdminServiceCollectionExtensions
{
public static IServiceCollection AddFluentCmsAdmin(this IServiceCollection services, Action<AdminOptions>? configure = null)
// The pages use Core's services and what the host reads from the request (the initiator, the theme); nothing of the admin's own yet.
public static IServiceCollection AddFluentCmsAdmin(this IServiceCollection services)
{
var options = new AdminOptions();
configure?.Invoke(options);
services.AddSingleton(options);

return services;
}
}
10 changes: 5 additions & 5 deletions src/FluentCMS.Admin/Components/App.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@inject NavigationManager Navigation
@inject IThemePreference Theme

<!DOCTYPE html>
<html lang="en" dir="@Direction" class="@ThemeClass">
<html lang="en" dir="@Direction" class="@Theme.Current">

<head>
<meta charset="utf-8" />
Expand All @@ -13,19 +14,18 @@

<body class="font-sans antialiased">
<Routes />
<ReconnectModal />
<script type="module" src="@Assets["_content/FluentCMS.Admin/js/admin.js"]"></script>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>

</html>

@code {
// The root element carries the direction and the theme. Until the shell stores a preference,
// both come from the query string so the spike and the showcase can switch them: ?dir=rtl&theme=dark.
// The root element carries the direction and the theme. Without a stored theme there is no class and the stylesheet
// follows the system preference. The direction comes from the query string until the UI culture sets it: ?dir=rtl.
private string Direction => Query("dir") == "rtl" ? "rtl" : "ltr";

private string ThemeClass => Query("theme") == "dark" ? "dark" : "";

private string? Query(string name)
{
var query = System.Web.HttpUtility.ParseQueryString(Navigation.ToAbsoluteUri(Navigation.Uri).Query);
Expand Down
1 change: 1 addition & 0 deletions src/FluentCMS.Admin/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<Button Type="submit" Variant="ghost" Size="sm">@L["Sign out"]</Button>
</form>
}
<ThemeToggle Class="@(Initiator.Current is UserInitiator ? null : "ms-auto")" />
</nav>
</header>
<main class="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
Expand Down
33 changes: 33 additions & 0 deletions src/FluentCMS.Admin/Components/Layout/ReconnectModal.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@inject IStringLocalizer<ReconnectModal> L

@* Blazor uses the element with this id instead of its default reconnect display, whose injected style element the CSP blocks.
It reports the state as a class on the element: app.css shows the matching part and admin.js runs the buttons.
Not modal, so the page stays readable and the client-side overlays keep working until the circuit is back. *@
<div id="components-reconnect-modal"
role="status"
aria-live="polite"
data-permanent
data-nosnippet
class="fixed end-4 bottom-4 z-50 w-80 max-w-[calc(100%-2rem)] flex-col gap-3 rounded-lg border bg-popover p-4 text-sm text-popover-foreground shadow-lg">
<p data-reconnect-when="show">@L["Connection lost. Rejoining the server…"]</p>
<p data-reconnect-when="retrying">@_retrying[0]<span id="components-seconds-to-next-attempt"></span>@_retrying.ElementAtOrDefault(1)</p>
<div data-reconnect-when="failed" class="space-y-3">
<p>@L["Could not rejoin the server."]</p>
<Button Size="sm" data-reconnect="retry">@L["Retry"]</Button>
</div>
<div data-reconnect-when="paused" class="space-y-3">
<p>@L["The session has been paused."]</p>
<Button Size="sm" data-reconnect="resume">@L["Resume"]</Button>
</div>
<div data-reconnect-when="resume-failed" class="space-y-3">
<p>@L["Could not resume the session."]</p>
<Button Size="sm" data-reconnect="reload">@L["Reload"]</Button>
</div>
</div>

@code {
private string[] _retrying = [];

// Blazor writes the countdown into the span, so the sentence is split where the translation puts the number.
protected override void OnInitialized() => _retrying = L["Rejoin failed. Trying again in {0} seconds."].Value.Split("{0}", 2);
}
20 changes: 20 additions & 0 deletions src/FluentCMS.Admin/Components/Layout/ThemeToggle.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@inject IStringLocalizer<ThemeToggle> L

@* shadcn's mode toggle. admin.js applies the choice to the root element and keeps it in the theme cookie, so it works
on static pages and while the circuit is down; "system" clears the choice. *@
<DropdownMenu>
<DropdownMenuTrigger Size="icon" Class="@Class" data-testid="theme-toggle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" aria-hidden="true"><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></svg>
<span class="sr-only">@L["Toggle theme"]</span>
</DropdownMenuTrigger>
<DropdownMenuContent Class="[position-area:block-end_span-inline-start]">
<DropdownMenuItem data-theme-choice="light">@L["Light"]</DropdownMenuItem>
<DropdownMenuItem data-theme-choice="dark">@L["Dark"]</DropdownMenuItem>
<DropdownMenuItem data-theme-choice="system">@L["System"]</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

@code {
[Parameter] public string? Class { get; set; }
}
Loading
Loading