Skip to main content

Built-in UI — Activation & CSS Customization

awesome-node-auth ships a zero-dependency, server-rendered HTML UI for all authentication flows. No React, no bundler, no build step required — the server injects your theme and configuration at request time.


Pages included

PagePathDescription
Login{apiPrefix}/ui/loginEmail/password + OAuth + magic link + SMS
Register{apiPrefix}/ui/registerNew account form (only when onRegister is set)
Forgot Password{apiPrefix}/ui/forgot-passwordPassword reset request form
Reset Password{apiPrefix}/ui/reset-passwordNew password form (via email token)
Magic Link{apiPrefix}/ui/magic-linkPasswordless email login
Verify Email{apiPrefix}/ui/verify-emailEmail verification landing page
2FA Challenge{apiPrefix}/ui/2faTOTP / SMS one-time password entry
Link Verify{apiPrefix}/ui/link-verifyOAuth account-linking confirmation
Account Conflict{apiPrefix}/ui/account-conflictOAuth provider/email conflict resolution

Step 1 — Install the UI router

buildUiRouter is separate from the auth API router. Mount both under the same prefix:

import express from 'express';
import { AuthConfigurator, buildUiRouter } from 'awesome-node-auth';
import { MyUserStore } from './my-user-store';

const app = express();
app.use(express.json());

const config: AuthConfig = {
// ... JWT secrets & expiry
ui: { loginUrl: '/auth/ui/login' }
};

const userStore = new MyUserStore();
const auth = new AuthConfigurator(config, userStore);

const routerOptions = {
// onRegister, oauthStrategies, etc.
};

// 1. Mount the REST API router
app.use('/auth', auth.router(routerOptions));

// 2. Mount the UI router at the same prefix + /ui
app.use(
'/auth/ui',
buildUiRouter({
authConfig: config,
routerOptions: routerOptions, // enables register/OAuth flags
apiPrefix: '/auth',
})
);

app.listen(3000, () => console.log('Running on http://localhost:3000'));

The built-in UI is now available at:

  • http://localhost:3000/auth/ui/login
  • http://localhost:3000/auth/ui/register
  • etc.

Step 2 — Configure AuthConfig.ui

All UI settings live under authConfig.ui. Every option is optional.

const auth = new AuthConfigurator(
{
// ... JWT secrets & expiry ...
ui: {
loginUrl: '/auth/ui/login',
registerUrl: '/auth/ui/register', // default
siteName: 'My App',
customLogo: 'https://cdn.example.com/logo.png',
primaryColor: '#6366f1', // indigo
secondaryColor: '#64748b',
bgColor: '#f0f4ff',
cardBg: '#ffffff',
bgImage: 'https://cdn.example.com/bg.jpg',
// Raw CSS injected into every page — override any rule or variable
customCss: `
:root { --primary-color: #6366f1; }
.auth-container { border-radius: 20px; }
`,
},
},
userStore
);

AuthConfig.ui options

OptionTypeDefaultDescription
loginUrlstring{apiPrefix}/ui/loginLogin page URL (used for redirects)
registerUrlstring{apiPrefix}/ui/registerRegister page URL
siteNamestring'Awesome Node Auth'Page title and <h1> heading
customLogostringURL of a logo image (displayed above the form)
primaryColorstring#4a90d9Main brand colour (buttons, headings, focus rings)
secondaryColorstring#6c757dSecondary colour (OAuth buttons, dividers)
bgColorstring#f8fafcPage background colour
bgImagestringPage background image URL (full-cover)
cardBgstring#ffffffForm/card background colour
customCssstringRaw CSS injected into every page <style> tag

Step 3 — Dynamic settings via ISettingsStore

For real-time theme changes without restarting the server (e.g. from an admin panel), implement ISettingsStore and pass it to buildUiRouter:

import { ISettingsStore } from 'awesome-node-auth';

class MongoSettingsStore implements ISettingsStore {
async getSettings() {
const doc = await db.collection('settings').findOne({ key: 'ui' });
return doc ?? {};
}
async updateSettings(settings: Partial<AuthSettings>) {
await db.collection('settings').updateOne(
{ key: 'ui' },
{ $set: { ...settings, key: 'ui' } },
{ upsert: true }
);
}
}

app.use('/auth/ui', buildUiRouter({
authConfig: config,
apiPrefix: '/auth',
settingsStore: new MongoSettingsStore(),
}));

Settings in the store override authConfig.ui. The built-in admin router's UI settings tab writes to the same ISettingsStore, so changes made in the admin panel appear instantly.


Step 4 — Custom logo uploads

To let admins upload a custom logo, pass an uploadDir to buildUiRouter. Uploaded files are served at {apiPrefix}/ui/assets/uploads/:

import path from 'path';

app.use('/auth/ui', buildUiRouter({
authConfig: config,
apiPrefix: '/auth',
uploadDir: path.resolve(__dirname, 'uploads/logo'),
}));

// Then set logoUrl to the uploaded path:
// logoUrl: '/auth/ui/assets/uploads/logo.png'

CSS customization reference

How theming works

The UI is styled with CSS custom properties (variables) defined in :root. When you set primaryColor, bgColor, etc. in AuthConfig.ui or ISettingsStore, the server renders an inline <style> block that overrides these variables before the stylesheet loads — eliminating any flash of unstyled content.

The customCss string is appended after the variable block, letting you override individual rules.

Full CSS variable reference

:root {
/* ── Colours ─────────────────────────────────────────── */
--primary-color: #4a90d9; /* buttons, headings, focus rings */
--primary-color-hover: #357abd; /* button hover state */
--secondary-color: #6c757d; /* OAuth buttons, dividers */
--secondary-color-hover:#5a6268; /* OAuth button hover */
--bg-color: #f8fafc; /* page background */
--bg-image: none; /* background image (url("…")) */
--card-bg: #ffffff; /* form card background */
--text-color: #1e293b; /* primary text */
--text-muted: #64748b; /* secondary/label text */
--error-color: #ef4444; /* error alerts */
--success-color: #22c55e; /* success alerts */
--border-color: #e2e8f0; /* input borders, dividers */
--input-focus: #4a90d9; /* input focus ring (same as primary by default) */
}

Overriding variables only — the minimal approach

ui: {
primaryColor: '#6366f1', // only changes --primary-color + --input-focus
}

Full CSS override via customCss

ui: {
customCss: `
/* Override variables */
:root {
--primary-color: #8b5cf6;
--primary-color-hover: #7c3aed;
--bg-color: #0f0f13;
--card-bg: #1a1a2e;
--text-color: #e2e8f0;
--text-muted: #94a3b8;
--border-color: #334155;
}

/* Override component rules */
.auth-container {
border-radius: 20px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

h1 { font-size: 28px; }
h2 { font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase; }

button[type="submit"] {
border-radius: 8px;
letter-spacing: 0.05em;
}

input {
background: var(--card-bg);
color: var(--text-color);
}
`,
}

Dark mode example

ui: {
bgColor: '#0f172a',
cardBg: '#1e293b',
customCss: `
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-color: #f1f5f9;
--text-muted: #94a3b8;
--border-color: #334155;
}
input { background: #0f172a; color: #f1f5f9; }
label { color: #cbd5e1; }
`,
}

CSS class reference

SelectorElementNotes
.auth-containerForm cardControls max-width (400px default), padding, border-radius
.logoLogo <img>Hidden if no logoUrl is set
.site-name<h1>Site name heading (set via siteName)
h2Sub-heading"Login", "Register", etc.
.alertAlert boxBase styles
.alert-errorError alertRed background
.alert-successSuccess alertGreen background
.form-groupField container<label> + <input> wrapper
inputAll form inputsIncludes email, password, OTP fields
button[type="submit"]Primary action buttonColoured by --primary-color
.btn-socialOAuth buttonsGoogle, GitHub
.divider"or" separatorBetween form and OAuth buttons
.footer-linksBottom link area"Forgot password?", "Sign up"

Step 5 — Custom assets directory

For complete control, replace all built-in HTML pages with your own by pointing uiAssetsDir at a local directory. Your directory must contain the same file names as the built-in pages.

app.use('/auth/ui', buildUiRouter({
authConfig: config,
apiPrefix: '/auth',
uiAssetsDir: path.resolve(__dirname, 'my-custom-ui'),
}));

Your HTML files still benefit from server-side config injection (window.__AUTH_CONFIG__, CSS variables) as long as they contain a </head> tag.


How auth.js activates automatically

Each built-in HTML page includes <script src="auth.js">, which:

  1. Reads window.__AUTH_CONFIG__ (injected by the server at render time — zero extra fetch)
  2. Intercepts all fetch() calls to add credentials: 'include' and X-CSRF-Token
  3. Handles 401/403 responses with transparent token refresh
  4. Exposes window.AwesomeNodeAuth for optional custom scripting

See Browser Client (auth.js) for the full window.AwesomeNodeAuth API.


Enabling features dynamically

The UI hides/shows features based on which strategies and handlers are configured:

FeatureShown when
Register linkrouterOptions.onRegister is set
Forgot passwordauthConfig.email.sendPasswordReset or .mailer is set
Magic linkauthConfig.email.sendMagicLink or .mailer is set
Google OAuthauthConfig.oauth.google is set
GitHub OAuthauthConfig.oauth.github is set
2FA pageauthConfig.twoFactor is set
Verify emailauthConfig.email.mailer is set and emailVerificationMode !== 'none'

Pass routerOptions to buildUiRouter so these flags are computed correctly:

const config: AuthConfig = { /* … */ };
const auth = new AuthConfigurator(config, userStore);

const routerOpts = {
onRegister: async (data) => { /* … */ },
// …
};

app.use('/auth', auth.router(routerOpts));
app.use('/auth/ui', buildUiRouter({ authConfig: config, routerOptions: routerOpts, apiPrefix: '/auth' }));

Redirecting after login

awesome-node-auth redirects to AuthConfig.ui.loginUrl when an unauthenticated user hits a protected route. After a successful login the default redirect target is / (the home page).

Customize both with AwesomeNodeAuth.init() in your own JS:

<script src="/auth/ui/assets/auth.js"></script>
<script>
AwesomeNodeAuth.init({ homeUrl: '/dashboard' });
</script>

Or server-side, via the window.__AUTH_CONFIG__ injection — the homeUrl can be baked in by buildUiRouter at render time if you extend getUiConfig.


Headless Mode — using auth.js without the UI pages

When embedding awesome-node-auth into an existing SPA or documentation site (e.g. Docusaurus, Next.js, Nuxt) you may want the client-side auth.js library (token refresh, session-expiry hooks) without the server-side HTML login / register pages.

Enable headless mode by setting ui.headless: true in your AuthConfig:

const config: AuthConfig = {
// ...
ui: {
headless: true,
loginUrl: '/auth/ui/login', // optional — ignored when headless
},
};

What changes in headless mode

FeatureNormal modeHeadless mode (ui.headless: true)
GET /auth/ui/loginReturns HTML login pageReturns 404
GET /auth/ui/registerReturns HTML register pageReturns 404
GET /auth/ui/auth.jsServed ✅Served ✅
GET /auth/ui/auth.cssServed ✅Served ✅
GET /auth/ui/config{ headless: false, … }{ headless: true, … }
window.location redirectsActiveDisabled — no-op handlers installed

Loading auth.js in a Docusaurus / SPA site

Mount the router in headless mode on your backend, then load auth.js from your frontend:

<!-- Load before your SPA bundle so the singleton is available immediately -->
<script src="/auth/ui/auth.js"></script>
<script>
// headless: true → installs no-op lifecycle handlers so auth.js never
// redirects window.location away from your SPA pages
AwesomeNodeAuth.init({
apiPrefix: '/auth',
headless: true,
});
</script>

auth.js automatically detects the headless: true flag from the /auth/ui/config response and suppresses all window navigation. You can still register your own handlers:

AwesomeNodeAuth.onSessionExpired(() => {
// show a custom "session expired" toast in your SPA
myToastService.show('Your session expired — please log in again.');
});

Cross-domain setup (API on a separate origin)

When the wiki / SPA is served from a different origin than the auth API, pass the full origin via apiPrefix:

// docusaurus.config.ts — inject auth.js from the API server
headTags: [
{
tagName: 'script',
attributes: { src: 'https://api.myapp.com/auth/ui/auth.js' },
},
{
tagName: 'script',
attributes: {},
innerHTML: `
if (window.AwesomeNodeAuth) {
window.AwesomeNodeAuth.init({
apiPrefix: 'https://api.myapp.com/auth',
headless: true,
});
}
`,
},
],

Angular / Next.js — do I need the built-in UI?

ClientRecommendation
Vanilla JS / jQuery / plain HTMLUse the built-in UI — zero setup
Next.js / Nuxt / SvelteKitBuilt-in UI works; or build your own pages and call the REST API
AngularUse the dedicated ng-awesome-node-auth library — it provides Guards, Interceptors, and a typed AuthService; auth.js is not needed in Angular projects

Production checklist
  • Set cookieOptions.secure: true so __Host- / __Secure- cookie prefixes are applied (cookie-tossing protection)
  • Set csrf: { enabled: true } when your frontend makes state-changing requests
  • Use ISettingsStore if you want runtime theme changes without server restarts
  • Pass uploadDir to accept logo uploads from the admin panel

Internationalization (i18n)

The built-in UI supports per-page text overrides via ITemplateStore. When a templateStore is configured and a UiTranslation entry exists for the current page, the UI router injects the translations into window.__AUTH_CONFIG__.translations at render time — no extra HTTP request needed.

How it works

Activating i18n

Pass a templateStore to buildUiRouter:

import { buildUiRouter, MemoryTemplateStore } from 'awesome-node-auth';

const templateStore = new MemoryTemplateStore();

// Pre-populate translations (or load from DB at startup)
await templateStore.updateUiTranslations('login', {
en: { title: 'Sign in', emailPlaceholder: 'Email address', submitBtn: 'Log in', forgotLink: 'Forgot password?' },
it: { title: 'Accedi', emailPlaceholder: 'Indirizzo email', submitBtn: 'Entra', forgotLink: 'Password dimenticata?' },
});

app.use('/auth/ui', buildUiRouter({
authConfig,
apiPrefix: '/auth',
templateStore, // ← enables i18n injection
}));

The language is resolved from the ?lang= query parameter first, then from email.mailer.defaultLang, and finally falls back to 'en'.

The data-i18n attribute

Every translatable element in the built-in HTML pages carries a data-i18n attribute:

<h2 data-i18n="title">Sign in</h2>
<input type="email" data-i18n="emailPlaceholder" placeholder="Email address" />
<button type="submit" data-i18n="submitBtn">Log in</button>
<a href="/auth/forgot-password" data-i18n="forgotLink">Forgot password?</a>

On page load auth.js calls AuthService.applyTranslations(), which:

  1. Reads window.__AUTH_CONFIG__.translations (flat { key: value } map for the current language)
  2. Iterates every [data-i18n] element
  3. Sets .textContent for regular elements or .placeholder for <input> / <textarea>

The built-in text remains in place as the default — it is only replaced when a matching translation key exists.

Page identifiers

Each UI page maps to a translation entry by its path segment. Use this identifier as the first argument to templateStore.updateUiTranslations():

PageUiTranslation.pageExample call
LoginloginupdateUiTranslations('login', { … })
RegisterregisterupdateUiTranslations('register', { … })
Forgot Passwordforgot-passwordupdateUiTranslations('forgot-password', { … })
Reset Passwordreset-passwordupdateUiTranslations('reset-password', { … })
Magic Linkmagic-linkupdateUiTranslations('magic-link', { … })
Verify Emailverify-emailupdateUiTranslations('verify-email', { … })
2FA Challenge2faupdateUiTranslations('2fa', { … })
Link Verifylink-verifyupdateUiTranslations('link-verify', { … })
Account Conflictaccount-conflictupdateUiTranslations('account-conflict', { … })

Managing translations via the Admin UI

Pass the same templateStore to createAdminRouter to edit translations live through the 📧 Email & UI tab:

app.use('/admin', createAdminRouter(userStore, {
accessPolicy: 'first-user',
jwtSecret: process.env.ACCESS_TOKEN_SECRET!,
templateStore,
}));

See Admin Panel → Email & UI tab and ITemplateStore API Reference.