This commit is contained in:
2026-09-09 17:35:23 +08:00
parent 2a98ae7556
commit 161af68fe3
1271 changed files with 58251 additions and 98639 deletions
@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Login and Authentication | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Login and Authentication | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-LoginAndAuth">
<h1 id="login-and-authentication">Login and Authentication</h1>
<p>The sign-in screen is the <code>/login</code> route. Whether anyone ever reaches it is decided by one back-end
setting — the <code>Enabled</code> flag of the <code>Auth</code> configuration section: with the flag off the service is
open and the route sends every visitor straight back out, and with it on a cookie-authentication
scheme plus a global authorization fallback policy lock every hub and every controller but the
authentication one — the whole <code>api/</code> surface apart from <code>api/auth</code> — until a visitor signs in. The
route hosts no Control-Tree branch and takes no <code>tree</code> query argument.</p>
<h2 id="the-enable-switch-and-its-two-defaults">The Enable Switch, and Its Two Defaults</h2>
<p><code>AuthConfig</code> binds the <code>Auth</code> section of configuration and is registered as one instance, so the
authentication controller and the startup decision read the same object. Two different values answer
the question &ldquo;is the gate on by default&rdquo;, and conflating them gets the answer wrong on a real
instance:</p>
<ul>
<li><strong>The class default is off.</strong> <code>Enabled</code> initialises to <code>false</code>, so a service whose configuration
carries no <code>Auth</code> section — or an empty one — runs with no login at all.</li>
<li><strong>The shipped configuration turns it on.</strong> <code>appsettings.json</code> and the Development overlay both set
<code>Enabled</code> to <code>true</code> and both supply one credential entry, so a service started from the repository
as it ships demands a sign-in.</li>
</ul>
<p>Three further settings live in the same section. <code>SessionHours</code> defaults to 8 and is raised to 1 at
registration if a smaller number is configured. <code>SlidingExpiration</code> defaults to <code>true</code>.
<code>Users</code> is a list of username and password pairs and defaults to empty.</p>
<p>That list is the whole credential store. Passwords are held in the configuration file in clear text
and compared verbatim — a first-match scan for an exact username <em>and</em> password — so there is no
hashing, no lockout, no rate limit and no user database behind it. The class documents the clear-text
form as deliberate, because the project also ships as sample code. Every entry grants the same
access; there are no roles. <code>Enabled</code> set to <code>true</code> over an empty list therefore admits nobody.</p>
<h2 id="what-the-gate-registers">What the Gate Registers</h2>
<p>The flag guards exactly two registrations at startup: a cookie authentication scheme, which becomes
the application's default and only scheme, and a global authorization fallback policy requiring an
authenticated user.</p>
<p>The fallback policy is what does the locking. It applies to every endpoint carrying no authorization
metadata of its own — every controller in the host assembly except the authentication one, and all
eight SignalR hub routes. <code>UseAuthentication</code> and <code>UseAuthorization</code> are added to the request pipeline
unconditionally and are no-ops when neither registration happened. See
<a class="xref" href="program-and-hosting.html">Program and Hosting</a> for the pipeline order this sits in.</p>
<p>Three places in the host carry <code>AllowAnonymous</code>, and they are the only ones that do:</p>
<ul>
<li>The authentication controller itself, marked at class level, so all three of its endpoints stay
reachable under the policy that locks everything else.</li>
<li>Both SPA fallbacks, so the client shell — which carries no data of its own — can load and show its
own login screen.</li>
</ul>
<p>Two pipeline stages answer before the authorization stage is reached and are unaffected by the
policy: static-file serving and the Swagger middleware. The first of those is what lets the login
screen render before anyone has signed in — the built bundle and the brand image it draws are
physical files under the web root.</p>
<p>With the gate off, none of this is registered: every controller, every hub and both fallbacks are
open, the login endpoint answers success without inspecting anything, the client's navigation guard
short-circuits, and the login route bounces the visitor away as it mounts.</p>
<h2 id="the-cookie">The Cookie</h2>
<p>The scheme issues a cookie named <code>HiNC.Auth</code>. It is <code>HttpOnly</code>, its <code>SameSite</code> mode is <code>Lax</code>, and its
secure policy is <em>same as request</em> — marked <code>Secure</code> when the request itself arrived over HTTPS and
issued plainly otherwise, so a plain-HTTP run on a local network still works. That decision is one of
the reasons forwarded headers are applied first in the pipeline, the HTTPS-redirection stage being
the other: behind a TLS-terminating reverse proxy the scheme must be read from <code>X-Forwarded-Proto</code>
rather than from the plain hop between proxy and service.</p>
<p>The ticket carries exactly one claim, the name of the matched user. Its lifetime is the configured
session length, refreshed on activity when sliding expiration is left on. Sign-in passes no
authentication properties, so the ticket is not marked persistent and the cookie carries no <code>Expires</code>
attribute: the browser holds it for the browser session, and the ticket's own expiry bounds a session
left open.</p>
<p>One event pair is overridden, and it is what makes a single-page client possible at all. A challenge
answers <strong>401</strong> and a refusal answers <strong>403</strong>, in place of the framework's default redirect to a
server-rendered login page. Every protected call therefore fails as data the client can read rather
than as a 302 that would arrive at the fetch layer as an HTML page.</p>
<h2 id="the-auth-endpoints">The Auth Endpoints</h2>
<p>The controller is routed at <code>api/auth</code> and holds three endpoints.</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Behaviour</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET status</code></td>
<td>Reports whether the gate is enabled, whether this caller is authenticated, the caller's name, and a version string. It is anonymous, so it answers before sign-in and whether or not the gate is on.</td>
</tr>
<tr>
<td><code>POST login</code></td>
<td>With the gate off, returns success without inspecting the body. With it on, scans the configured users for an exact match and either answers 401 with an English message plus the stable code <code>InvalidCredentials</code>, or signs the caller in.</td>
</tr>
<tr>
<td><code>POST logout</code></td>
<td>Signs the caller out when the gate is on, and reports &ldquo;not authenticated&rdquo; either way.</td>
</tr>
</tbody>
</table>
<p>The version the status endpoint reports is the HiNc assembly version,
<a class="xref" href="../../api/Hi.MachiningProcs.MachiningProject.html#Hi_MachiningProcs_MachiningProject_ApiVersion_">ApiVersion</a><small>(API)</small>. Because the endpoint is
anonymous, that value is readable before sign-in, which is what lets the login screen and the menu bar
show the same version mark.</p>
<p>Nothing else in the host reads the signed-in identity — the status endpoint is its only consumer. Every
application service is registered as a process-wide singleton — the host adds no scoped or transient
registration of its own — so authentication decides admission rather than identity: two signed-in
browsers drive the same project and the same session. See <a class="xref" href="session-state.html">Session State</a> for what that
shared state consists of.</p>
<h2 id="the-client-side">The Client Side</h2>
<h3 id="the-store">The Store</h3>
<p>A Pinia store holds the whole client-side picture: whether the gate is enabled, whether this session is
authenticated, the user name, the version string, and a ready flag set once the first status probe has
resolved. <code>refresh</code> reads the status endpoint; <code>login</code> and <code>logout</code> call their endpoints and update the
same fields; a fourth action flips the store to logged-out with no round trip, for the interceptor
below. Every transition re-drives the hub gate.</p>
<h3 id="the-navigation-guard">The Navigation Guard</h3>
<p>One global <code>beforeEach</code> guard runs the whole client-side decision, in order:</p>
<ol>
<li>If the store is not ready yet, hydrate it from the status endpoint, inside a <code>try</code>/<code>catch</code> that
swallows the failure. The ready flag is set only on success, so a failed probe is retried on the
next navigation.</li>
<li>If the gate reports disabled, allow the navigation.</li>
<li>If the target is the login route, allow it.</li>
<li>If the session is not authenticated, redirect to the login route with the blocked target's full
path — query arguments, <code>tree</code> included — parked in a <code>redirect</code> argument.</li>
<li>Otherwise allow it.</li>
</ol>
<p><strong>What the guard does not do</strong> is protect anything. It is a navigation redirect: it runs on router
navigations only, so it has no bearing on a direct <code>api/</code> request, on a hub negotiate, or on a static
asset, all of which are the fallback policy's business. It reads no per-route metadata, so there is no
list of public routes beyond the login route itself and no notion of a role. And it fails open by
construction — when the status probe throws, the store keeps the gate disabled and the navigation is
allowed, so a back-end hiccup cannot lock a user out of an installation that has no gate.</p>
<h3 id="the-401-interceptor">The 401 Interceptor</h3>
<p>A boot file wraps the global <code>fetch</code> once for the whole application. Every API module, and SignalR's
own negotiate, call that global, so one wrapper covers a session that expires mid-use: on any 401
response, and only while the store says the gate is enabled, it marks the store logged out and pushes
the login route with the current full path in <code>redirect</code>, unless the router is already there. The
response is handed back unchanged, so the calling code still sees its own failure and handles it.</p>
<p>Boot order is load-bearing here. The interceptor's boot file is declared ahead of the i18n one, and
boot files are awaited in declaration order, so the locale probe made during startup goes through the
patched <code>fetch</code>.</p>
<h3 id="the-hub-gate">The Hub Gate</h3>
<p>The shared-hub registry carries one tri-state gate that the store drives on every transition: undecided
while the status probe is still resolving, closed while the gate is on and the session is signed out,
and open for a signed-in session or a service with no gate. Undecided holds, so a first-paint race
never fires a negotiate that is certain to fail; closed keeps every hub idle rather than letting the
never-give-up reconnect schedule hammer a <code>/negotiate</code> that answers 401 by design. The store releases
the gate even when the status probe fails, so a probe hiccup never strands the hubs of a service that
has no login. Opening the gate resumes the connections a caller had already asked for, without the
caller asking again.</p>
<h3 id="after-a-successful-sign-in">After a Successful Sign-In</h3>
<p>The login screen navigates with a full page load rather than a router navigation. The application's
one-shot wiring — the project hub subscription and the first project-status fetch — sits behind a
watcher on the auth predicate and runs once per page load, so reloading is what re-runs it with the
cookie present.</p>
<h2 id="signing-out">Signing Out</h2>
<p>The logout control sits at the right of the menu bar and renders only while the gate is enabled <em>and</em>
the session is authenticated. It is labelled with the signed-in user name, falling back to <code>Logout</code>
when the status carries no name, and its tooltip reads <code>Log out</code>. It posts the logout endpoint and
then, in a <code>finally</code>, hard-navigates to <code>/login</code> — so a failed request still tears the page down and
rebuilds the client from scratch, rather than leaving the singletons of an ended session wired up.</p>
<h2 id="layout">Layout</h2>
<p>The route renders outside the shell layout, in a Quasar layout of its own — the same arrangement the
catch-all not-found route uses. Nothing of the application frame is present: no menu bar, and therefore
no Project, Page or Preference menu and no language submenu; no cached page panel; no
routine-progress footer. The browser tab reads <code>Login - HiNC</code>.</p>
<ul>
<li>Login Page — a single card centred on an empty page
<ul>
<li>Brand Section
<ul>
<li>Brand image, above the literal title <code>HiNC</code></li>
<li>Version caption, <code>v</code> followed by the version string — drawn only when the status probe returned
one</li>
<li>Prompt caption — <code>Please sign in to continue</code></li>
</ul>
</li>
<li>Separator</li>
<li>Sign-In Form
<ul>
<li><code>Username</code> Text Field — autofocused on arrival, disabled while a sign-in is in flight</li>
<li><code>Password</code> Text Field — masked, with a trailing eye icon that toggles the text visible; the icon's
accessible label alternates between <code>Show password</code> and <code>Hide password</code></li>
<li>Error caption — drawn in the negative colour, and only after a failed attempt</li>
<li><code>Sign In</code> Button — submits the form and shows a spinner while the request is in flight</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Pressing Enter in either field submits the same form. Neither field carries a validation rule, so an
empty pair is submitted and refused by the server like any other wrong pair. The error caption shows
the localized &ldquo;Incorrect username or password&rdquo; whenever the server pairs its refusal with the
<code>InvalidCredentials</code> code, and the server's own English message for any other refusal that carries
one.</p>
<p>Two cases send the visitor away again as soon as the screen mounts. The mount hook hydrates the auth
status if the first probe has not resolved yet, and then replaces the route with the parked <code>redirect</code>
target — or with <code>/</code>, which redirects on to the Execution page — when the gate reports disabled or when
the session is already authenticated.</p>
<p>The locale the screen paints in is whatever the boot sequence resolved, because the stored language
preference is served by an endpoint the fallback policy locks. With a browser-local cached locale the
screen paints in that; without one the awaited probe fails and the locale falls back to the browser's
own language, and to English after that. The screen's own strings ship in all three locales.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>Common/AuthConfig.cs</code> — the bound shape: the enable flag and its class default, the session length,
the sliding-expiration flag, and the clear-text user list.</li>
<li><code>Common/AuthController.cs</code> — the anonymous <code>api/auth</code> controller: the status projection, the
first-match credential scan with its message and code pair, the single name claim it signs in, and
the sign-out.</li>
<li><code>Program.cs</code> — the flag-guarded registration of the cookie scheme and the fallback policy, the cookie
name and flags, the 401 / 403 event overrides, the forwarded-headers configuration ahead of them, and
the two anonymous SPA fallbacks.</li>
<li><code>wwwroot-src/src/pages/LoginPage.vue</code> — the screen: its own Quasar layout, the card and its fields,
the bounce on mount, and the full page load after a successful sign-in.</li>
<li><code>wwwroot-src/src/router/routes.ts</code> — the login route outside the shell layout, and the title key the
tab shows.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the navigation guard: the once-only hydration, the disabled and
login-route short circuits, and the redirect carrying the full path; also the retitle that composes
the tab text from the route's title key.</li>
<li><code>wwwroot-src/src/stores/auth.ts</code> — the client-side state, the four actions, and the hub gate each of
them re-drives.</li>
<li><code>wwwroot-src/src/api/auth.ts</code> — the three typed endpoint wrappers, and the mapping of the server's
refusal code onto the localized message.</li>
<li><code>wwwroot-src/src/boot/auth.ts</code> — the global <code>fetch</code> wrapper that turns a 401 into a logged-out store
and a push to the login route.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — the boot-file order that puts the interceptor ahead of the locale
probe.</li>
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the locale resolution the login screen paints in, and its fallback
chain when the language endpoint answers 401.</li>
<li><code>wwwroot-src/src/composables/useSharedHub.ts</code> — the tri-state hub gate and what each of its states
does to a connection.</li>
<li><code>wwwroot-src/src/App.vue</code> — the one-shot wiring held behind the auth predicate, which is what the
post-sign-in page load re-runs.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the logout control and the version badge, both drawn
from the auth store.</li>
<li><code>wwwroot-src/src/i18n/en/auth.ts</code> — the screen's English strings.</li>
<li><code>wwwroot-src/src/i18n/en/menu.ts</code> — the <code>Logout</code> label the control falls back to, and its <code>Log out</code>
tooltip.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the <code>Login</code> route title the browser tab resolves.</li>
</ul>
<h3 id="hiapi-engine">HiAPI Engine</h3>
<ul>
<li><code>HiNc/MachiningProcs/MachiningProject.cs</code> — the assembly version the status endpoint reports and the
screen shows.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="program-and-hosting.html">Program and Hosting</a> — the host that binds the <code>Auth</code> section and registers the scheme,
the fallback policy and the anonymous carve-outs described here</li>
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the shell this screen renders outside of, and the menu bar that carries
the logout control and the version badge</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>