Skip to content

Commit c18dcfc

Browse files
committed
1.8.2 - fixing Admin UI access
1 parent 1809cc2 commit c18dcfc

11 files changed

Lines changed: 290 additions & 143 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
55

66
---
77

8+
## [1.8.2] — 2026-04-01
9+
10+
### Added
11+
- **Self-Contained Admin Authentication** — the Admin UI now handles its own login and logout via internal `POST /admin/login` and `POST /admin/logout` routes. This removes dependencies on the main application's auth router, making the Admin Panel truly autonomous.
12+
- **Root/Bootstrap User Support** — added `AdminOptions.rootUser` (email + password hash) for permanent emergency access.
13+
- **Bootstrap Mode** — if `adminSecret` is configured, the Admin login form allows password-only access (leaving email blank).
14+
- **Dynamic Cookie Prefix Detection** — the admin guard now automatically detects and handles secure cookie prefixes (`__Host-`, `__Secure-`) based on the environment and `AdminOptions.cookiePrefix`.
15+
16+
### Fixed
17+
- **Admin UI Logout** — fixed a regression where the logout button would attempt to call the main auth API instead of the local admin logout handler.
18+
- **Cookie Prefix Conflicts** — resolved issues where admin sessions were not persisted correctly when running behind a proxy or in production with secure cookies.
19+
20+
---
21+
822
## [1.8.1] — 2026-04-01
923

1024
### Added

demo/advanced-telemetry-webhooks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev": "ts-node src/index.ts"
1010
},
1111
"dependencies": {
12-
"awesome-node-auth": "^1.8.1",
12+
"awesome-node-auth": "^1.8.2",
1313
"express": "^5.0.0",
1414
"express-rate-limit": "^7.0.0",
1515
"mongodb": "^6.0.0"

demo/angular-ssr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@angular/platform-server": "^19.2.21",
2222
"@angular/router": "^19.2.21",
2323
"@angular/ssr": "^19.2.21",
24-
"awesome-node-auth": "^1.8.1",
24+
"awesome-node-auth": "^1.8.2",
2525
"cookie-parser": "^1.4.7",
2626
"express": "^4.21.2",
2727
"rxjs": "^7.8.2",

demo/express-angular-spa/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@angular/platform-browser": "^19.2.21",
1919
"@angular/platform-browser-dynamic": "^19.2.21",
2020
"@angular/router": "^19.2.21",
21-
"awesome-node-auth": "^1.8.1",
21+
"awesome-node-auth": "^1.8.2",
2222
"cookie-parser": "^1.4.7",
2323
"express": "^4.21.2",
2424
"rxjs": "^7.8.2",

demo/express-vanilla/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"start": "node server.js"
88
},
99
"dependencies": {
10-
"awesome-node-auth": "^1.8.1",
10+
"awesome-node-auth": "^1.8.2",
1111
"cookie-parser": "^1.4.7",
1212
"express": "^4.21.2"
1313
},

demo/nestjs-fullstack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"@nestjs/common": "^10.4.15",
1111
"@nestjs/core": "^10.4.15",
1212
"@nestjs/platform-express": "^10.4.15",
13-
"awesome-node-auth": "^1.8.1",
13+
"awesome-node-auth": "^1.8.2",
1414
"cookie-parser": "^1.4.7",
1515
"reflect-metadata": "^0.2.2",
1616
"rxjs": "^7.8.2"

demo/nextjs-fullstack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"serve": "next start"
1111
},
1212
"dependencies": {
13-
"awesome-node-auth": "^1.8.1",
13+
"awesome-node-auth": "^1.8.2",
1414
"next": "^15.3.0",
1515
"react": "^18.3.1",
1616
"react-dom": "^18.3.1"

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "awesome-node-auth",
3-
"version": "1.8.1",
3+
"version": "1.8.2",
44
"description": "Database-agnostic JWT authentication and communication bus for Node.js",
55
"main": "dist/index.js",
66
"scripts": {

src/router/admin.router.ts

Lines changed: 150 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import multer from 'multer';
33
import path from 'path';
44
import fs from 'fs';
55
import jwt from 'jsonwebtoken';
6+
import bcrypt from 'bcryptjs';
67
import { IUserStore } from '../interfaces/user-store.interface';
78
import { ISessionStore } from '../interfaces/session-store.interface';
89
import { IRolesPermissionsStore } from '../interfaces/roles-permissions-store.interface';
@@ -95,6 +96,27 @@ export interface AdminOptions {
9596
*/
9697
linkedAccountsStore?: ILinkedAccountsStore;
9798

99+
/**
100+
* Optional prefix for the authentication cookies (e.g. `__Host-` or `__Secure-`).
101+
* If provided, the guard will look for `${cookiePrefix}accessToken`.
102+
* If not provided, it will try the default variants.
103+
*
104+
* @since 1.8.1
105+
*/
106+
cookiePrefix?: string;
107+
108+
/**
109+
* Optional root user for the Admin UI.
110+
* Useful for bootstrapping or in environments without local users.
111+
*
112+
* @since 1.8.1
113+
*/
114+
rootUser?: {
115+
email: string;
116+
/** Bcrypt-hashed password. */
117+
passwordHash: string;
118+
};
119+
98120
/**
99121
* Optional API Key store — enables the 🔑 API Keys tab in the admin UI.
100122
* Requires `IApiKeyStore.listAll` for listing and optionally `delete` for hard deletion.
@@ -193,6 +215,7 @@ function buildPolicyGuard(
193215
jwtSecret: string | undefined,
194216
rbacStore?: IRolesPermissionsStore,
195217
loginPath?: string,
218+
cookiePrefix?: string,
196219
): RequestHandler {
197220
return async (req: Request, res: Response, next) => {
198221
// 'open' — no auth required at all
@@ -206,7 +229,13 @@ function buildPolicyGuard(
206229
const bearerToken = req.headers.authorization?.startsWith('Bearer ')
207230
? req.headers.authorization.slice(7)
208231
: undefined;
209-
const cookieToken = (req.cookies as Record<string, string> | undefined)?.accessToken;
232+
233+
const cookies = (req.cookies as Record<string, string> | undefined) ?? {};
234+
const cookieName = cookiePrefix ? `${cookiePrefix}accessToken` : undefined;
235+
const cookieToken = cookieName
236+
? cookies[cookieName]
237+
: (cookies['__Host-accessToken'] ?? cookies['__Secure-accessToken'] ?? cookies['accessToken']);
238+
210239
const rawToken = bearerToken ?? cookieToken;
211240

212241
if (rawToken) {
@@ -253,6 +282,18 @@ function buildPolicyGuard(
253282
return;
254283
}
255284

285+
// Handle root/bootstrap override
286+
if (payload['isRoot'] === true) {
287+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
288+
(req as any).user = {
289+
id: userId,
290+
email: (payload['email'] as string) || 'root@admin',
291+
isAdmin: true,
292+
} as BaseUser;
293+
next();
294+
return;
295+
}
296+
256297
let user: BaseUser | null = null;
257298
try {
258299
user = await userStore.findById(userId);
@@ -324,6 +365,8 @@ function buildAdminHtml(baseUrl: string, features: {
324365
showLogin?: boolean;
325366
/** Path to the main auth API router (for login calls). */
326367
authApiPrefix?: string;
368+
/** Optional prefix for cookies. */
369+
cookiePrefix?: string;
327370
}): string {
328371
// Config object injected as window.__ADMIN_CONFIG__ and read by admin.js.
329372
const cfg = JSON.stringify({
@@ -342,6 +385,7 @@ function buildAdminHtml(baseUrl: string, features: {
342385
uploadBaseUrl: features.uploadBaseUrl,
343386
sessionBased: !!features.sessionBased,
344387
authApiPrefix: features.authApiPrefix || '/auth',
388+
cookiePrefix: features.cookiePrefix,
345389
});
346390

347391
const showLogin = features.showLogin || !features.sessionBased;
@@ -417,6 +461,7 @@ export function createAdminRouter(
417461
options.jwtSecret,
418462
options.rbacStore,
419463
options.loginPath,
464+
options.cookiePrefix,
420465
);
421466
sessionBased = options.accessPolicy !== 'open';
422467
} else if (options.adminSecret) {
@@ -431,6 +476,82 @@ export function createAdminRouter(
431476
guard = (_req, _res, next) => next();
432477
}
433478

479+
const secret = options.jwtSecret;
480+
481+
// ── Local Login Handler (Self-contained Auth) ──────────────────────────
482+
if (sessionBased && secret) {
483+
router.post('/login', async (req, res) => {
484+
const { email, password } = req.body;
485+
if (!password) {
486+
res.status(400).json({ error: 'Password required' });
487+
return;
488+
}
489+
490+
let authedUser: { id: string; email: string; isRoot?: boolean } | null = null;
491+
492+
// 1. Check Root User
493+
if (options.rootUser && email === options.rootUser.email) {
494+
if (await bcrypt.compare(password, options.rootUser.passwordHash)) {
495+
authedUser = { id: 'root', email: options.rootUser.email, isRoot: true };
496+
}
497+
}
498+
499+
// 2. Check Admin Secret (Bootstrap override)
500+
if (!authedUser && options.adminSecret && (!email || email === 'admin')) {
501+
if (password === options.adminSecret) {
502+
authedUser = { id: 'admin', email: 'admin@bootstrap', isRoot: true };
503+
}
504+
}
505+
506+
// 3. Fallback to UserStore (standard login)
507+
if (!authedUser && email) {
508+
try {
509+
const user = await userStore.findByEmail(email);
510+
if (user && user.password && await bcrypt.compare(password, user.password)) {
511+
authedUser = { id: user.id || '', email: user.email };
512+
}
513+
} catch { /* ignore */ }
514+
}
515+
516+
if (!authedUser) {
517+
res.status(401).json({ error: 'Invalid credentials' });
518+
return;
519+
}
520+
521+
// Sign JWT
522+
const token = jwt.sign(
523+
{ sub: authedUser.id, email: authedUser.email, isRoot: authedUser.isRoot },
524+
secret,
525+
{ expiresIn: '24h' },
526+
);
527+
528+
// Set cookie
529+
const cookieName = (options.cookiePrefix ?? '') + 'accessToken';
530+
const cookieOptions = (options as any).cookieOptions || {
531+
httpOnly: true,
532+
secure: process.env.NODE_ENV === 'production',
533+
sameSite: 'lax',
534+
path: '/',
535+
};
536+
res.cookie(cookieName, token, cookieOptions);
537+
538+
res.json({ success: true });
539+
});
540+
541+
// ── Local Logout Handler ───────────────────────────────────────────────
542+
router.post('/logout', (req, res) => {
543+
const cookieName = (options.cookiePrefix ?? '') + 'accessToken';
544+
const cookieOptions = (options as any).cookieOptions || {
545+
httpOnly: true,
546+
secure: process.env.NODE_ENV === 'production',
547+
sameSite: 'lax',
548+
path: '/',
549+
};
550+
res.clearCookie(cookieName, cookieOptions);
551+
res.json({ success: true });
552+
});
553+
}
554+
434555
// Resolve effective uploadBaseUrl for both UI script and API responses
435556
let effectiveUploadBaseUrl = options.uploadBaseUrl || '';
436557
if (!effectiveUploadBaseUrl && options.apiPrefix && options.uploadDir) {
@@ -502,33 +623,35 @@ export function createAdminRouter(
502623
// When using legacy adminSecret: the HTML is served without auth (the client-side JS handles login).
503624
const htmlRoute: RequestHandler[] = sessionBased
504625
? [guard, (_req: Request, res: Response) => {
505-
const needsAuth = ((_req as any).adminNeedsAuth === true);
506-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
507-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
508-
res.setHeader('Pragma', 'no-cache');
509-
res.setHeader('Expires', '0');
510-
res.send(buildAdminHtml(_req.baseUrl, {
511-
sessions: featSessions, roles: featRoles, tenants: featTenants, metadata: featMetadata,
512-
twoFAPolicy: featTwoFAPolicy, control: featControl, linkedAccounts: featLinkedAccounts,
513-
apiKeys: featApiKeys, webhooks: featWebhooks, templates: featTemplates, upload: featUpload,
514-
uploadBaseUrl: effectiveUploadBaseUrl, sessionBased,
515-
showLogin: needsAuth,
516-
authApiPrefix: options.apiPrefix,
517-
}));
518-
}]
626+
const needsAuth = ((_req as any).adminNeedsAuth === true);
627+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
628+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
629+
res.setHeader('Pragma', 'no-cache');
630+
res.setHeader('Expires', '0');
631+
res.send(buildAdminHtml(_req.baseUrl, {
632+
sessions: featSessions, roles: featRoles, tenants: featTenants, metadata: featMetadata,
633+
twoFAPolicy: featTwoFAPolicy, control: featControl, linkedAccounts: featLinkedAccounts,
634+
apiKeys: featApiKeys, webhooks: featWebhooks, templates: featTemplates, upload: featUpload,
635+
uploadBaseUrl: effectiveUploadBaseUrl, sessionBased,
636+
showLogin: needsAuth,
637+
authApiPrefix: options.apiPrefix,
638+
cookiePrefix: options.cookiePrefix,
639+
}));
640+
}]
519641
: [(_req: Request, res: Response) => {
520-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
521-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
522-
res.setHeader('Pragma', 'no-cache');
523-
res.setHeader('Expires', '0');
524-
res.send(buildAdminHtml(_req.baseUrl, {
525-
sessions: featSessions, roles: featRoles, tenants: featTenants, metadata: featMetadata,
526-
twoFAPolicy: featTwoFAPolicy, control: featControl, linkedAccounts: featLinkedAccounts,
527-
apiKeys: featApiKeys, webhooks: featWebhooks, templates: featTemplates, upload: featUpload,
528-
uploadBaseUrl: effectiveUploadBaseUrl, sessionBased,
529-
authApiPrefix: options.apiPrefix,
530-
}));
531-
}];
642+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
643+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
644+
res.setHeader('Pragma', 'no-cache');
645+
res.setHeader('Expires', '0');
646+
res.send(buildAdminHtml(_req.baseUrl, {
647+
sessions: featSessions, roles: featRoles, tenants: featTenants, metadata: featMetadata,
648+
twoFAPolicy: featTwoFAPolicy, control: featControl, linkedAccounts: featLinkedAccounts,
649+
apiKeys: featApiKeys, webhooks: featWebhooks, templates: featTemplates, upload: featUpload,
650+
uploadBaseUrl: effectiveUploadBaseUrl, sessionBased,
651+
authApiPrefix: options.apiPrefix,
652+
cookiePrefix: options.cookiePrefix,
653+
}));
654+
}];
532655
router.get('/', ...htmlRoute);
533656

534657
// GET /admin/api/ping — health / auth check

0 commit comments

Comments
 (0)