diff --git a/AUTHELIA_INTEGRATION.md b/AUTHELIA_INTEGRATION.md new file mode 100644 index 0000000..59131c3 --- /dev/null +++ b/AUTHELIA_INTEGRATION.md @@ -0,0 +1,495 @@ +# Authelia Portal — LotusGuild Terminal Design System Integration + +This document covers everything that had to be **written from scratch** to theme the +Authelia authentication portal using the LotusGuild Terminal Design System v1.2. +It is a companion to `base.css` / `base.js` for future integrations with third-party +applications that use their own component frameworks. + +--- + +## Why a separate file at all? + +`base.css` targets exclusively `.lt-*` class names — the design system's own prefix. +Authelia's frontend is a React single-page application built on +**Material UI (MUI) v5**, which generates its own stable class names like +`.MuiCard-root`, `.MuiButton-containedPrimary`, etc. +Bridging the two requires a translation layer: the same design tokens and visual +patterns from `base.css`, re-expressed against MUI's class structure. + +Additionally, Authelia v4.x (latest: 4.39.x) does **not** support native CSS +injection — `server.asset_path` only handles `logo.png` and `favicon.ico`. +The CSS is injected via **nginx `sub_filter`** in Nginx Proxy Manager, which rewrites +Authelia's HTML on the fly to include a `` tag before ``. + +--- + +## Deployment architecture + +``` +Browser + └─► https://auth.lotusguild.org (NPM — LXC 139) + ├─ GET /custom.css ──────── served from /data/custom_assets/authelia-custom.css + │ (nginx alias, no upstream request) + └─ GET /* ──────────────── proxied to Authelia (LXC 167 :9091) + HTML response is rewritten by sub_filter: + → +``` + +**NPM config changed:** `/data/nginx/proxy_host/29.conf` (auth.lotusguild.org proxy host) + +```nginx +# Serve LotusGuild Terminal custom CSS +location = /custom.css { + alias /data/custom_assets/authelia-custom.css; + add_header Content-Type "text/css"; + add_header Cache-Control "public, max-age=3600"; +} + +location / { + ...existing headers... + + # Prevent upstream gzip so sub_filter can inspect the body + proxy_set_header Accept-Encoding ""; + + # Inject CSS link before + sub_filter '' ''; + sub_filter_once on; + + include /etc/nginx/conf.d/include/proxy.conf; +} +``` + +> **Warning:** NPM regenerates proxy_host conf files when you edit a proxy host in +> the UI. If host 29 is ever modified through NPM, re-apply the `sub_filter` block +> and the `/custom.css` location manually, or set the config via NPM's +> "Advanced" tab before saving. + +**Authelia config changed:** `/etc/authelia/configuration.yml` — added `asset_path` +under `server:` so Authelia serves the resized logo and favicon: + +```yaml +server: + address: tcp://0.0.0.0:9091 + asset_path: /etc/authelia/assets +``` + +Assets in `/etc/authelia/assets/`: +- `logo.png` — 256 × 256 PNG resized from Tinker Tickets `assets/images/favicon.png` +- `favicon.ico` — 32 × 32 ICO from the same source + +--- + +## What is NOT in `base.css` and had to be written + +The sections below are ordered as they appear in `authelia-custom.css`. + +--- + +### 1. Re-declaring design tokens with `!important` + +**In `base.css`:** `:root { ... }` is declared once, without `!important`. + +**What had to be added:** The entire `:root` block is repeated in `custom.css` because +Authelia's HTML is a self-contained SPA — `base.css` is never loaded. More critically, +MUI injects its own inline styles and CSS-in-JS rules at very high specificity. +Every property that needs to override MUI requires `!important`. `base.css` never +uses `!important` because it owns its namespace; here we are guests in MUI's DOM. + +```css +/* base.css does NOT use !important anywhere */ +body { background-color: var(--bg-primary); } + +/* custom.css must fight MUI's inline styles */ +body { background-color: var(--bg-primary) !important; } +``` + +The token values themselves are identical to `base.css` — only the override +mechanism is new. + +--- + +### 2. Universal box-sizing reset with `!important` + +**In `base.css`:** `*, *::before, *::after { box-sizing: border-box; }` — no flag needed +because `base.css` loads first. + +**What had to be added:** + +```css +*, *::before, *::after { + box-sizing: border-box !important; +} +``` + +MUI components set `box-sizing` inline on some elements. Without the flag the +clip-path geometry breaks on inputs and buttons. + +--- + +### 3. MUI typography selectors + +**In `base.css`:** Typography rules target `h1–h6`, `a`, `p`, `body` — standard HTML +elements. No MUI class names exist in the design system. + +**What had to be added:** Authelia renders text almost exclusively through MUI's +`` component, which produces elements with `.MuiTypography-root` and +variant classes. Without targeting these, all text inherits MUI's Roboto font and +light grey colour instead of JetBrains Mono and `--text-primary`. + +```css +.MuiTypography-root, +.MuiInputBase-input, +.MuiFormLabel-root, +.MuiFormHelperText-root, +label, p, span, div { + font-family: var(--font-mono) !important; + color: var(--text-primary) !important; +} + +h1, h2, h3, h4, h5, h6, +.MuiTypography-h5, +.MuiTypography-h6 { + color: var(--accent-orange) !important; + text-shadow: var(--glow-orange) !important; + ... +} +``` + +--- + +### 4. `.MuiCard-root` / `.MuiPaper-root` — the login card + +**In `base.css`:** `.lt-card` implements the terminal card with `clip-path`, border, +background, and the `::before` corner triangle accent. + +**What had to be added:** Authelia wraps its login form in a MUI `` (which +extends ``). Neither selector exists in `base.css`. The visual result is +identical to `.lt-card` — the clip-path polygon, cyan border, box-glow, and +corner triangle are all copied — but they target different class names and require +`!important` throughout to override MUI's elevation shadows and `border-radius: 4px`. + +```css +.MuiCard-root, +.MuiPaper-root { + background: var(--bg-card) !important; + border: 1px solid var(--border-color) !important; + border-radius: 0 !important; /* ← overrides MUI default */ + clip-path: polygon(...) !important; /* same geometry as .lt-card */ + ... +} + +.MuiCard-root::before, +.MuiPaper-root::before { + /* same corner triangle as .lt-card::before */ +} +``` + +--- + +### 5. Logo element — `img[alt="Authelia"]` + +**In `base.css`:** No equivalent. The design system has no logo slot component. + +**What had to be added:** Authelia renders the portal logo as +`Authelia`. This selector targets it +specifically to apply the cyan drop-shadow and orange hover glow. Without it the +logo renders without any terminal aesthetic treatment. + +```css +img[alt="Authelia"] { + filter: drop-shadow(0 0 12px rgba(0,212,255,0.4)) !important; + transition: filter 0.25s ease !important; +} +img[alt="Authelia"]:hover { + filter: drop-shadow(0 0 18px rgba(255,107,0,0.5)) !important; +} +``` + +--- + +### 6. MUI outlined input family + +**In `base.css`:** `.lt-input`, `.lt-select`, `.lt-textarea` — custom elements with +`clip-path`, terminal background, and cyan focus ring. + +**What had to be added:** MUI's outlined text field is a composition of three +separate elements that each need individual targeting: + +| MUI class | What it controls | Equivalent in `base.css` | +|-----------|-----------------|--------------------------| +| `.MuiOutlinedInput-root` | Outer wrapper — background, clip-path | `.lt-input` outer | +| `.MuiOutlinedInput-notchedOutline` | The SVG border element | `.lt-input` border | +| `.MuiInputBase-input` | The actual `` inside | `.lt-input` inner text | +| `.MuiInputLabel-root` | Floating label | `.lt-label` | +| `.MuiFormHelperText-root` | Error / hint text below field | `.lt-form-hint` | + +MUI splits the border rendering into a separate SVG `
` element +(`.MuiOutlinedInput-notchedOutline`), which requires its own border-color override. +`base.css` has no equivalent splitting since `.lt-input` is a single element with a +CSS border. + +The `.Mui-focused` and `.Mui-error` state classes also need explicit overrides because +MUI applies its own colour through CSS-in-JS with high specificity. + +```css +.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: var(--accent-cyan) !important; + box-shadow: var(--box-glow-cyan) !important; +} +.MuiFormHelperText-root.Mui-error { + color: var(--accent-red) !important; + text-shadow: var(--glow-red) !important; +} +``` + +Additionally, `caret-color` is set explicitly on the input because MUI does not +expose this through its theme — the blinking text cursor would otherwise be white. + +--- + +### 7. MUI button family + +**In `base.css`:** `.lt-btn`, `.lt-btn-primary`, `.lt-btn-danger`, `.lt-btn-ghost` — +all using `clip-path` hexagon cuts, transparent backgrounds, and border+glow styling. + +**What had to be added:** MUI uses four separate button variant classes. The primary +(sign-in) button is `.MuiButton-containedPrimary` which by default renders as a +solid filled rectangle. The entire button appearance — transparent background, +border, clip-path, text-transform, letter-spacing — has to be explicitly overridden +because MUI's `contained` variant applies an opaque `background-color` inline. + +```css +/* MUI default: background-color: #1976d2; border-radius: 4px; box-shadow: ... */ +.MuiButton-containedPrimary { + background: transparent !important; /* fight the inline fill */ + border: 1px solid var(--accent-orange-border) !important; + border-radius: 0 !important; + clip-path: polygon(...) !important; + ... +} +``` + +Text/link buttons (`.MuiButton-text`) are separate from contained buttons in MUI but +map to the same `.lt-btn-ghost` pattern in `base.css`. + +--- + +### 8. MUI checkbox and switch + +**In `base.css`:** `.lt-checkbox`, `.lt-switch` — fully custom-drawn using CSS only. + +**What had to be added:** Authelia uses MUI's `` (remember-me) and +`` (settings toggles). These are SVG-based components. Because MUI controls +their colour through injected CSS variables, the only reliable override is to target +`.Mui-checked` state classes and use `!important`. There is no equivalent split +between `track` and `thumb` in `base.css`. + +```css +.MuiSwitch-switchBase.Mui-checked { color: var(--accent-cyan) !important; } +.MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track { + background-color: var(--accent-cyan) !important; + opacity: 0.5 !important; +} +``` + +--- + +### 9. MUI alert / notification banners + +**In `base.css`:** `.lt-inline-*` for inline messages (`.lt-inline-success`, +`.lt-inline-error`, etc.) and `.lt-toast-*` for toasts. These use a left border +accent pattern. + +**What had to be added:** Authelia uses MUI `` for login errors, success +messages, and 2FA prompts. MUI Alert has its own severity system with four variant +classes (`.MuiAlert-standardError`, etc.). Each severity needs explicit colour, +background, and border-color overrides. The `border-left: 3px solid currentColor` +pattern is adapted from `base.css`'s inline message style but applied to MUI's +class names with `clip-path: none !important` to cancel MUI's rounded corners. + +--- + +### 10. MUI select, menu, and popover + +**In `base.css`:** `.lt-select` for the select element itself; no dropdown overlay +component exists since the design system uses native `` renders its dropdown as a `` +element portalled to `` with class `.MuiMenu-paper`. Without targeting this +separately, the dropdown popup inherits the browser default white background and +Roboto font, appearing completely unstyled. `.MuiMenuItem-root` hover and selected +states also need individual rules. + +--- + +### 11. OTP / 2FA digit inputs — `input[type="tel"]`, `input[type="number"]` + +**In `base.css`:** No equivalent. The design system has no numeric code input. + +**What had to be added:** Authelia's one-time-password entry uses `` +elements arranged in a row. These need oversized, centered, cyan-glowing characters +with wide letter-spacing to render like a retro code display. + +```css +input[type="tel"], +input[type="number"] { + font-size: 1.2rem !important; + font-weight: 700 !important; + color: var(--accent-cyan) !important; + text-shadow: var(--glow-cyan) !important; + letter-spacing: 0.15em !important; + text-align: center !important; + caret-color: var(--accent-orange) !important; +} +``` + +--- + +### 12. MUI stepper — 2FA flow breadcrumb + +**In `base.css`:** `.lt-stepper`, `.lt-step`, `.lt-step-num` — a custom CSS stepper +for multi-step wizard flows. + +**What had to be added:** Authelia renders a MUI `` at the top of the 2FA +flow showing steps like "Username → Password → TOTP". MUI Stepper uses distinct +classes for label, icon, and connector that don't map 1-to-1 to `.lt-step-*`. +The step icon is an SVG circle rendered by React so it can only be tinted via +`color` and `filter: drop-shadow(...)`. The connector line between steps is a +separate `
` with class `.MuiStepConnector-line`. + +```css +.MuiStepIcon-root.Mui-active { + color: var(--accent-orange) !important; + filter: drop-shadow(0 0 6px rgba(255,107,0,0.6)) !important; +} +.MuiStepIcon-root.Mui-completed { + color: var(--accent-green) !important; +} +``` + +--- + +### 13. MUI linear and circular progress + +**In `base.css`:** `.lt-progress` / `.lt-progress-bar` — a standard CSS progress bar +using `width` transitions. `.lt-spinner` — a rotating pseudo-element spinner. + +**What had to be added:** Authelia uses MUI `` during API calls and +`` as the main loading spinner. MUI Linear Progress animates its +own internal elements (`.MuiLinearProgress-bar`) with keyframe animations; it cannot +be replaced with the `.lt-progress` pattern. The cyan-to-orange gradient and +`box-shadow` glow are new here — `base.css`'s progress bar uses a solid +`--accent-orange` fill. + +```css +.MuiLinearProgress-bar { + background: linear-gradient(90deg, var(--accent-cyan), var(--accent-orange)) !important; + box-shadow: 0 0 8px rgba(0,212,255,0.5) !important; +} +``` + +--- + +### 14. MUI tooltip + +**In `base.css`:** `.lt-tooltip` — a CSS-only tooltip shown via `:hover` on a +`[data-tooltip]` attribute. + +**What had to be added:** MUI `` portals its bubble to `` with class +`.MuiTooltip-tooltip`. It is a separate DOM node, not a pseudo-element, so +`.lt-tooltip` styles cannot reach it. A standalone rule sets the terminal background, +monospace font, and squared border. + +--- + +### 15. `*:focus-visible` global ring + +**In `base.css`:** Focus rings are defined individually per component +(`.lt-btn:focus-visible`, `.lt-input:focus-visible`, etc.). + +**What had to be added:** A single global rule is more practical here because +Authelia contains many interactive MUI elements not known at design time, and +adding per-component rules for every possible focusable element would be brittle. + +```css +*:focus-visible { + outline: 2px solid var(--accent-cyan) !important; + outline-offset: 2px !important; + box-shadow: var(--box-glow-cyan) !important; +} +``` + +--- + +### 16. Branding footer watermark — `#root::after` + +**In `base.css`:** No equivalent. The design system does not add page-level +watermarks. + +**What had to be added:** Authelia has no footer. The `#root::after` pseudo-element +attaches a fixed-position text watermark reading +`LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG` at the bottom of the viewport. +This uses `--text-muted`, `--font-mono`, uppercase, and wide letter-spacing to match +the terminal aesthetic without adding any HTML. + +```css +#root::after { + content: 'LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG'; + position: fixed; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + font-size: 0.55rem; + letter-spacing: 0.18em; + color: var(--text-muted); + opacity: 0.6; + pointer-events: none; +} +``` + +--- + +## Patterns shared with `base.css` (not new) + +The following are **not** documented above because they are direct re-uses of +existing `base.css` patterns, only re-targeted to new selectors: + +| Pattern | `base.css` source | Re-used in `custom.css` for | +|---------|------------------|-----------------------------| +| Dot-grid background | `html { background-image: radial-gradient(...) }` | `body` | +| Scanlines overlay | `body::before` | `body::before` (same rule, `!important` added) | +| Corner vignette | `body::after` | `body::after` (same rule, `!important` added) | +| Card clip-path polygon | `.lt-card { clip-path: polygon(...) }` | `.MuiCard-root` | +| Card corner triangle | `.lt-card::before` | `.MuiCard-root::before` | +| Button clip-path hexagon | `.lt-btn { clip-path: polygon(...) }` | `.MuiButton-containedPrimary` | +| Input clip-path cut | `.lt-input { clip-path: polygon(...) }` | `.MuiOutlinedInput-root` | +| Scrollbar | `::-webkit-scrollbar-*` section 45 | identical, `border-radius: 0` added | +| Link colours | `a { color: var(--accent-cyan) }` | `a` (same, `!important` added) | +| Divider | `.lt-divider` border-color | `.MuiDivider-root` | + +--- + +## Notes for future third-party integrations + +1. **Always check for `!important` requirements.** CSS-in-JS frameworks (MUI, Emotion, + styled-components) inject styles at runtime with high specificity. Without + `!important`, most design token overrides will lose to injected inline styles. + +2. **The split border problem.** MUI (and many component libraries) separate the + border from the element it visually belongs to. Always inspect the actual DOM + before writing selectors — what looks like one element is often three. + +3. **Portalled overlays.** Dropdowns, tooltips, and modals in MUI are rendered into + a separate DOM tree (`` portal). Scoped selectors inside a card or form + won't reach them. Target their root classes (`.MuiMenu-paper`, `.MuiTooltip-tooltip`) + directly. + +4. **State classes.** MUI uses `.Mui-focused`, `.Mui-checked`, `.Mui-error`, + `.Mui-active`, `.Mui-completed`, `.Mui-disabled` as state modifiers. These are the + equivalent of `:focus`, `:checked`, etc. but applied by JavaScript — pseudo-class + selectors alone will sometimes not fire. + +5. **SVG-based components.** Checkboxes, radio buttons, step icons, and spinners are + SVGs injected by React. They cannot be styled with `background`, `border`, or + `clip-path`. Use `color` (SVG `currentColor` inheritance) and `filter: + drop-shadow()` instead.