feat(dashboard-auth): clarify remote dashboard login flow

- add explicit access modes so remote users see setup guidance instead of an ambiguous login

- redesign the login page with remote setup messaging, password visibility, and light/dark controls

- cover the access-state contract and login UI with focused tests
This commit is contained in:
Tam Nhu Tran committed 2026-04-06 17:01:49 -04:00
1 parent 9b3e85c5d9
commit d130972916
7 files changed
+600 -89

No files matched your search

+49 -7
View File
@@ -7,6 +7,7 @@ import { Router, type Request, type Response } from 'express';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import { getDashboardAuthConfig } from '../../config/unified-config-loader';
import type { DashboardAuthConfig } from '../../config/unified-config-types';
import { isLoopbackRemoteAddress, loginRateLimiter } from '../middleware/auth-middleware';
/**
@@ -24,6 +25,52 @@ function timingSafeEqual(a: string, b: string): boolean {
const router = Router();
export type DashboardAccessMode = 'open' | 'login' | 'setup';
export interface DashboardAccessState {
authRequired: boolean;
authEnabled: boolean;
authConfigured: boolean;
isLocalAccess: boolean;
accessMode: DashboardAccessMode;
}
export function resolveDashboardAccessState(
authConfig: DashboardAuthConfig,
remoteAddress: string | undefined
): DashboardAccessState {
const isLocalAccess = isLoopbackRemoteAddress(remoteAddress);
const authConfigured = Boolean(authConfig.username && authConfig.password_hash);
if (authConfig.enabled && authConfigured) {
return {
authRequired: true,
authEnabled: true,
authConfigured: true,
isLocalAccess,
accessMode: 'login',
};
}
if (!authConfig.enabled && isLocalAccess) {
return {
authRequired: false,
authEnabled: false,
authConfigured,
isLocalAccess: true,
accessMode: 'open',
};
}
return {
authRequired: true,
authEnabled: authConfig.enabled,
authConfigured,
isLocalAccess,
accessMode: 'setup',
};
}
/**
* POST /api/auth/login
* Authenticate user with username/password.
@@ -94,15 +141,10 @@ router.post('/logout', (req: Request, res: Response) => {
*/
router.get('/check', (req: Request, res: Response) => {
const authConfig = getDashboardAuthConfig();
const isLocal = isLoopbackRemoteAddress(req.socket.remoteAddress);
// When auth is not configured and access is remote, the dashboard API
// endpoints return 403. Signal auth-required so the UI can show the
// login/setup page instead of a silently broken dashboard.
const effectiveAuthRequired = authConfig.enabled || !isLocal;
const accessState = resolveDashboardAccessState(authConfig, req.socket.remoteAddress);
res.json({
authRequired: effectiveAuthRequired,
...accessState,
authenticated: req.session?.authenticated ?? false,
username: req.session?.username ?? null,
});