MMLib.Alvo.Admin
The Alvo admin dashboard: server-interactive Blazor components and the design system they ship with.
The Alvo admin dashboard: server-interactive Blazor components and the design system they ship with.
Namespace MMLib.Alvo.Admin
Section titled “Namespace MMLib.Alvo.Admin”AlvoAdmin
Section titled “AlvoAdmin”static class
The paths and names the dashboard publishes, for a host that has to spell one of them.
The same argument AlvoAdminAssets makes: spelled by hand these are strings that compile either way and fail only in the browser; spelled here they move with the package.
BasePath
Section titled “BasePath”public const string BasePath = "/admin";Where the dashboard is served.
See AlvoAdminOptions for why this is a constant and not an option.
ConfigurationSection
Section titled “ConfigurationSection”public const string ConfigurationSection = "Alvo:Admin:Dashboard";The configuration section the dashboard’s options bind from.
SetPasswordEndpoint
Section titled “SetPasswordEndpoint”public const string SetPasswordEndpoint = "/admin/set-password/submit";Where the set-password form posts.
A form post to an endpoint the host maps, for SignInEndpoint’s reason: redeeming a token needs the identity package, which this package does not reference. The path is here so the form and the endpoint cannot drift.
SetPasswordPath
Section titled “SetPasswordPath”public const string SetPasswordPath = "/admin/set-password";The page a person opens from a credential token’s link to set their password.
Public so a CLI or an agent can compose the link from what the Management API returns: the token is returned raw, and the link is {origin}{PathBase}/admin/set-password#email=…&token=…, both values escaped with EscapeDataString.
The token rides in the fragment, never in the query string. A browser sends no fragment to the server, to a proxy’s access log or in Referer, so the bearer credential stays out of every log without depending on anyone’s log configuration. The page reads it with a small script and removes it from the address bar and the history.
SignInEndpoint
Section titled “SignInEndpoint”public const string SignInEndpoint = "/admin/sign-in/submit";Where the sign-in form posts, and where signing out posts.
A form post, not a circuit call, and the reason is a hard one. Signing in writes an authentication cookie, and a cookie is written onto a response — a server-interactive component runs over a WebSocket, long after the response that carried the page has completed, so SignInAsync inside a circuit throws. Every Blazor application solves this the same way: a statically rendered form that posts to an ordinary endpoint.
The endpoint itself is not in this package. Signing in needs ASP.NET Core Identity’s SignInManager, and this package holds no reference to MMLib.Alvo.Identity — the host owns both, so the host maps it. The path is here so the form and the endpoint cannot drift.
SignInPath
Section titled “SignInPath”public const string SignInPath = "/admin/sign-in";The sign-in screen.
SignOutEndpoint
Section titled “SignOutEndpoint”public const string SignOutEndpoint = "/admin/sign-out";Where signing out posts.
A post, not a link: a sign-out reachable by GET can be triggered by any page that embeds an image pointing at it.
AlvoAdminAssets
Section titled “AlvoAdminAssets”static class
The static web assets this package publishes, by the path a host references them at.
Why these are a public contract and not a convention. A Razor Class Library serves its wwwroot under _content/<assembly name>/, and a host that wants the dashboard styled has to write that path into a <link> tag. Spelled by hand it is a string that compiles either way and fails only in the browser; spelled here it moves with the package and the compiler catches a rename.
The design system is CSS and the screens are Razor components, and neither is a type a consumer is meant to call — some sixty component classes are technically public (the Razor SDK gives every one of them that accessibility; see Properties/AssemblyInfo.cs), but [EditorBrowsable(Never)] on the whole namespace keeps them out of a consumer’s IntelliSense and out of the contract this package keeps stable. This type, alongside AlvoAdminOptions, AlvoAdminClaims, IAlvoAdminCallerResolver and the two registration/mapping extension methods, is what actually is.
public static string Mark { get; }The mark on its own — a rounded square carrying the prompt glyph.
Module
Section titled “Module”public static string Module { get; }The interop module the components import — the bridge between Script’s keyboard map and a Blazor circuit.
Separate from Script deliberately. That one runs before hydration and knows nothing about Blazor, which is what makes it correct for a theme applied before the first paint. This one is an ES module, imported once per circuit on the first call that needs it and released with the circuit.
Rooted at the origin, unlike the other two, and it has to be. Those are written into a <link> and a <script src>, where the document’s <base> resolves a relative path. This one is handed to import(), and a specifier that starts with neither / nor ./ is a bare specifier — a package name, which a browser with no import map cannot resolve. The import rejects, the dashboard logs it at error and every keyboard shortcut and overlay gesture does nothing — a page that renders but will not answer ⌘K, which reads as a broken application rather than as a missing file.
Script
Section titled “Script”public static string Script { get; }The three browser concerns the design system owns: the stored theme, the density, and the keyboard map.
A host loads this in <head> rather than at the end of the body: it applies the stored theme before the first paint, and a theme applied after paint is a flash. It carries no framework and takes over nothing Blazor renders — it publishes alvo:* events and lets the component that owns the surface decide what they mean.
StyleSheet
Section titled “StyleSheet”public static string StyleSheet { get; }The design system — tokens, both themes, and every component class.
The one stylesheet a host that styles its own page with Alvo’s look links. The dashboard’s own document also links its component library through an internal, layered sheet beneath this one; that is the document’s business, not a host’s, so it is not listed here.
Wordmark
Section titled “Wordmark”public static string Wordmark { get; }The mark with the name and the line beneath it, for a screen with room for it.
AlvoAdminClaims
Section titled “AlvoAdminClaims”static class
The claim types the dashboard reads off a signed-in operator.
The host mints these when it signs somebody in, and the dashboard reads them. Spelled here so the two halves cannot drift — the same argument AlvoAdminAssets makes about a path, applied to a claim type, and for the same reason: a mismatched claim type is a string that compiles on both sides and produces a screen that quietly shows “no tenant” to somebody who has one.
There is no role claim type here because there is nothing to choose: roles are Role, which is what ClaimsPrincipal.IsInRole reads and what every ASP.NET Core authorization primitive assumes.
Tenant
Section titled “Tenant”public const string Tenant = "alvo:tenant";The operator’s tenant, as the canonical Guid string, when they hold one.
AlvoAdminEndpointRouteBuilderExtensions
Section titled “AlvoAdminEndpointRouteBuilderExtensions”static class
Maps the dashboard.
MapAlvoAdmin
Section titled “MapAlvoAdmin”public static IEndpointRouteBuilder MapAlvoAdmin(this IEndpointRouteBuilder endpoints)Maps the dashboard’s components, unless Enabled says not to.
The caller is responsible for three pieces of pipeline this cannot add for it, because all three are middleware and middleware is ordered by the host rather than by an endpoint: UseStaticFiles or MapStaticAssets (the design system travels as a static web asset of this package), UseAntiforgery (Blazor’s form handling requires it), and authentication. MMLib.Alvo.Host does all three.
endpoints— The route builder to map into.
Returns: The route builder, for chaining.
AlvoAdminOptions
Section titled “AlvoAdminOptions”class
What a host may change about the dashboard.
Deliberately small. The dashboard is one screen set over one contract (IAlvoManagement); everything it shows comes from the descriptor, the schema and capabilities, so there is nothing here to configure that the descriptor does not already own.
There is no configurable base path, and that is a decision rather than an omission. A routable Razor component’s @page directive is a compile-time constant, so a mount point read from configuration would have to be implemented as a nested pipeline branch — and every link, redirect and NavigationManager call inside the dashboard would then have to learn about a prefix the router does not know. The dashboard therefore lives at BasePath, and a deployment that needs it elsewhere moves the whole application with UsePathBase, which is the mechanism ASP.NET Core already has for this and the one the standalone host already exposes as Alvo:PathBase.
DocsPath
Section titled “DocsPath”public string? DocsPath { get; set; }Where this host serves its interactive API documentation, or null when it serves none.
It arrives from the host because the dashboard cannot know it. Serving a document is a hosting decision — the core deliberately never calls AddOpenApi — and this package references Abstractions and nothing else, so it can neither read the standalone host’s route constant nor guess an embedded host’s. The alternative was the one already in the code: the API tab stated /openapi/v1.json in prose, on every deployment, including the ones that serve nothing there.
null is the default, and it means no link rather than a broken one. An embedded host mounts its documentation where it likes, or not at all; a link rendered on the assumption is a 404 an operator reaches from a screen that promised a contract.
Enabled
Section titled “Enabled”public bool Enabled { get; set; }Whether the dashboard is mapped at all. Defaults to true.
An embedded host that references this package for its design system alone, or a deployment that fronts Alvo with its own console, turns it off here rather than by not calling MapAlvoAdmin — so the switch is configuration, readable in one place beside the rest of the Alvo: section, rather than a line of code somebody has to find.
OpenApiPath
Section titled “OpenApiPath”public string? OpenApiPath { get; set; }Where this host serves the OpenAPI document itself, or null when it serves none.
Separate from DocsPath because the two have different readers: a person opens the docs UI, and an agent fetches the document. Alvo is built for the second one, so the raw URL is offered rather than buried one click inside a viewer.
AlvoAdminServiceCollectionExtensions
Section titled “AlvoAdminServiceCollectionExtensions”static class
Registers the dashboard.
AddAlvoAdmin
Section titled “AddAlvoAdmin”public static IServiceCollection AddAlvoAdmin(this IServiceCollection services, Action<AlvoAdminOptions>? configure = null)Adds the Razor components the dashboard is built from, and the server-interactive render mode they run in.
It adds no session re-check of its own. The dashboard’s chrome — which screens the router renders — follows the host’s AuthenticationStateProvider, and a signed-in session is re-checked only if the host’s provider does it. MMLib.Alvo.Identity’s cookie sign-in registers one that does; a host that mints its own sessions owns their revalidation, and without it a disabled operator’s open tab keeps navigating until its next page load. Authority never lingers either way: every management and data call re-resolves its caller through IAlvoAdminCallerResolver and is refused at once.
The set-password post is the host’s to map. Access hands a credential token over as a link to SetPasswordPath, whose form posts to SetPasswordEndpoint. Redeeming it needs the identity package, which this one does not reference, so MapAlvoAdmin does not map it: MMLib.Alvo.Host does, and an embedded host that registers IAlvoUserAdministration maps its own POST there over AlvoSignIn.SetPasswordAsync (the host’s AlvoAdminSetPassword shows the order its checks must run in). Until it does, Access finds no such route in the host’s endpoints and hands over the bare token instead, with one sentence saying the host has no set-password page — never a link to a form that posts nowhere.
services— The service collection to register into.configure— Configures the dashboard.
Returns: services, for chaining.
IAlvoAdminCallerResolver
Section titled “IAlvoAdminCallerResolver”interface
Turns the operator a host has signed in into the caller Alvo authorizes.
It is also the seam that keeps an embedded host honest. A host with its own users and its own roles implements this over its own store and the dashboard works unchanged; nothing in the screens assumes ASP.NET Core Identity, because nothing in them can see it.
A null answer is a refusal, not an anonymous caller. An implementation that cannot map the operator — no such user, disabled, or a tenant it may not confirm — returns nothing, and the dashboard shows the refusal. Minting a caller with no tenant instead would silently widen “you may not act there” into “you act everywhere unscoped”.
It is asked on every call, and it is the only re-check the dashboard makes. Whether an open tab’s session is re-checked — whether the shell drops to sign-in once the operator is disabled — is the host’s AuthenticationStateProvider’s business, not this port’s: a host that mints its own sessions owns their revalidation. What this port guarantees is that the calls behind the screens are refused the moment it stops answering with a caller.
ResolveAsync
Section titled “ResolveAsync”ValueTask<AlvoPrincipal?> ResolveAsync(ClaimsPrincipal signedIn, CancellationToken cancellationToken)Resolves the signed-in operator’s Alvo caller.
signedIn— The principal the host’s authentication produced.cancellationToken— Cancels the lookup.
Returns: The caller, or null when this operator is not one.