Files
web_template/AUTHELIA_INTEGRATION.md
T

496 lines
18 KiB
Markdown
Raw Normal View History

2026-04-14 16:30:39 -04:00
# 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 `<link>` tag before `</head>`.
---
## 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:
</head> → <link rel="stylesheet" href="/custom.css"></head>
```
**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 </head>
sub_filter '</head>' '<link rel="stylesheet" href="/custom.css"></head>';
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 `h1h6`, `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
`<Typography>` 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 `<Card>` (which
extends `<Paper>`). 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
`<img alt="Authelia" src="./static/media/logo.png">`. 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 `<input>` 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 `<fieldset>` 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 `<Checkbox>` (remember-me) and
`<Switch>` (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 `<Alert>` 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 `<select>`.
**What had to be added:** MUI's `<Select>` renders its dropdown as a `<Paper>`
element portalled to `<body>` 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 `<input type="tel">`
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 `<Stepper>` 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 `<hr>` 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 `<LinearProgress>` during API calls and
`<CircularProgress>` 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 `<Tooltip>` portals its bubble to `<body>` 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 (`<body>` 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.