password based service info auth (better than the first one we had)
This commit is contained in:
@@ -25,6 +25,15 @@ NODE_ENV=production
|
||||
# Defaults to true. Set to false to disable login and make the info page public.
|
||||
# ENABLE_INFO_PAGE_LOGIN=true
|
||||
|
||||
# Authentication mode for the service info page. (token | password)
|
||||
# If 'token', any valid user token (via `getUser()`) is used (requires GATEKEEPER='user_token' mode).
|
||||
# If 'password', SERVICE_INFO_PASSWORD is used.
|
||||
# Defaults to 'token' if ENABLE_INFO_PAGE_LOGIN is true.
|
||||
# SERVICE_INFO_AUTH_MODE=token
|
||||
|
||||
# Password for the service info page if SERVICE_INFO_AUTH_MODE is 'password'.
|
||||
# SERVICE_INFO_PASSWORD=your-service-info-password
|
||||
|
||||
# The route name used to proxy requests to APIs, relative to the Web site root.
|
||||
# PROXY_ENDPOINT_ROUTE=/proxy
|
||||
|
||||
|
||||
@@ -435,6 +435,10 @@ type Config = {
|
||||
loginImageUrl?: string;
|
||||
/** Whether to enable the token-based login page for the service info page. Defaults to true. */
|
||||
enableInfoPageLogin?: boolean;
|
||||
/** Authentication mode for the service info page. (token | password) */
|
||||
serviceInfoAuthMode: "token" | "password";
|
||||
/** Password for the service info page if serviceInfoAuthMode is 'password'. */
|
||||
serviceInfoPassword?: string;
|
||||
};
|
||||
|
||||
// To change configs, create a file called .env in the root directory.
|
||||
@@ -554,6 +558,8 @@ export const config: Config = {
|
||||
},
|
||||
loginImageUrl: getEnvWithDefault("LOGIN_IMAGE_URL", ""),
|
||||
enableInfoPageLogin: getEnvWithDefault("ENABLE_INFO_PAGE_LOGIN", true),
|
||||
serviceInfoAuthMode: getEnvWithDefault("SERVICE_INFO_AUTH_MODE", "token") as Config["serviceInfoAuthMode"],
|
||||
serviceInfoPassword: getEnvWithDefault("SERVICE_INFO_PASSWORD", undefined),
|
||||
} as const;
|
||||
|
||||
function generateSigningKey() {
|
||||
@@ -691,6 +697,25 @@ export async function assertConfigIsValid() {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.enableInfoPageLogin) {
|
||||
if (!["token", "password"].includes(config.serviceInfoAuthMode)) {
|
||||
throw new Error(
|
||||
`Invalid SERVICE_INFO_AUTH_MODE: ${config.serviceInfoAuthMode}. Must be 'token' or 'password'.`
|
||||
);
|
||||
}
|
||||
if (config.serviceInfoAuthMode === "password" && !config.serviceInfoPassword) {
|
||||
throw new Error(
|
||||
"SERVICE_INFO_AUTH_MODE is 'password' but SERVICE_INFO_PASSWORD is not set."
|
||||
);
|
||||
}
|
||||
// If service info login is token-based, gatekeeper must be 'user_token' mode for getUser() to be effective.
|
||||
if (config.serviceInfoAuthMode === "token" && config.gatekeeper !== "user_token") {
|
||||
throw new Error(
|
||||
"SERVICE_INFO_AUTH_MODE is 'token' for info page login, but GATEKEEPER is not 'user_token'. User token authentication will not work."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure forks which add new secret-like config keys don't unwittingly expose
|
||||
// them to users.
|
||||
for (const key of getKeys(config)) {
|
||||
@@ -765,6 +790,7 @@ export const OMITTED_KEYS = [
|
||||
"powTokenPurgeHours",
|
||||
"loginImageUrl",
|
||||
"enableInfoPageLogin",
|
||||
"serviceInfoPassword",
|
||||
] satisfies (keyof Config)[];
|
||||
type OmitKeys = (typeof OMITTED_KEYS)[number];
|
||||
|
||||
|
||||
+29
-14
@@ -110,7 +110,7 @@ function renderLoginPage(csrf: string, error?: string) {
|
||||
padding:30px;width:100%;max-width:400px;text-align:center;}
|
||||
.logo-image{max-width:200px;margin-bottom:20px;}
|
||||
.form-group{margin-bottom:20px;}
|
||||
input[type=text]{width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;
|
||||
input[type=text], input[type=password]{width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;
|
||||
box-sizing:border-box;font-size:16px;}
|
||||
button{background:#4caf50;color:#fff;border:none;padding:12px 20px;border-radius:4px;
|
||||
cursor:pointer;font-size:16px;width:100%;}
|
||||
@@ -120,8 +120,8 @@ function renderLoginPage(csrf: string, error?: string) {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #2c2c2c; color: #e0e0e0; }
|
||||
.login-container { background: #383838; box-shadow: 0 4px 12px rgba(0,0,0,0.4); border: 1px solid #4a4a4a; }
|
||||
input[type=text] { background: #4a4a4a; color: #e0e0e0; border: 1px solid #5a5a5a; }
|
||||
input[type=text]::placeholder { color: #999; }
|
||||
input[type=text], input[type=password] { background: #4a4a4a; color: #e0e0e0; border: 1px solid #5a5a5a; }
|
||||
input[type=text]::placeholder, input[type=password]::placeholder { color: #999; }
|
||||
button { background: #007bff; } /* Using a blue for dark mode button */
|
||||
button:hover { background: #0056b3; }
|
||||
.error-message { color: #ff8a80; } /* Lighter red for errors in dark mode */
|
||||
@@ -134,7 +134,9 @@ function renderLoginPage(csrf: string, error?: string) {
|
||||
${errBlock}
|
||||
<form method="POST" action="${LOGIN_ROUTE}">
|
||||
<div class="form-group">
|
||||
<input type="text" id="token" name="token" required placeholder="Your token">
|
||||
${config.serviceInfoAuthMode === "password"
|
||||
? `<input type="password" id="password" name="password" required placeholder="Service Password">`
|
||||
: `<input type="text" id="token" name="token" required placeholder="Your token">`}
|
||||
<input type="hidden" name="_csrf" value="${csrf}">
|
||||
</div>
|
||||
<button type="submit">Access Dashboard</button>
|
||||
@@ -343,17 +345,30 @@ infoPageRouter.use(
|
||||
|
||||
/* login attempt */
|
||||
infoPageRouter.post(LOGIN_ROUTE, (req, res) => {
|
||||
const token = (req.body.token || "").trim();
|
||||
|
||||
const user = getUser(token); // returns undefined if invalid
|
||||
if (!user) {
|
||||
return res
|
||||
.status(401)
|
||||
.send(renderLoginPage(res.locals.csrfToken, "Invalid token. Please try again."));
|
||||
if (config.serviceInfoAuthMode === "password") {
|
||||
const password = (req.body.password || "").trim();
|
||||
// Simple string comparison; for production, consider a timing-safe comparison library
|
||||
if (config.serviceInfoPassword && password === config.serviceInfoPassword) {
|
||||
req.session!.infoPageAuthed = true;
|
||||
return res.redirect("/");
|
||||
} else {
|
||||
return res
|
||||
.status(401)
|
||||
.send(renderLoginPage(res.locals.csrfToken, "Invalid password. Please try again."));
|
||||
}
|
||||
} else {
|
||||
// Token-based authentication (using any valid user token)
|
||||
const token = (req.body.token || "").trim();
|
||||
const user = getUser(token); // returns undefined if invalid
|
||||
if (user) {
|
||||
req.session!.infoPageAuthed = true;
|
||||
return res.redirect("/");
|
||||
} else {
|
||||
return res
|
||||
.status(401)
|
||||
.send(renderLoginPage(res.locals.csrfToken, "Invalid token. Please try again."));
|
||||
}
|
||||
}
|
||||
|
||||
req.session!.infoPageAuthed = true;
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
/* GET / – either login form or info page */
|
||||
|
||||
Reference in New Issue
Block a user