Spaces:
Running
Running
fix: auth on env-builder, single login for terminal, new login UI
Browse files- Protect /env-builder and /env-builder.js with requireAuth (same as /app)
- Inject Authorization: token <JUPYTER_TOKEN> when proxying to JupyterLab
so JupyterLab skips its own login screen — one password instead of two
- New login page: HuggingClaw-style dark card (matches project aesthetic)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- health-server.js +33 -33
health-server.js
CHANGED
|
@@ -116,42 +116,36 @@ function loginUrl(nextPath) {
|
|
| 116 |
|
| 117 |
function renderLoginPage(nextPath, errorMessage = "") {
|
| 118 |
const safeNext = sanitizeNext(nextPath);
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
return `<!doctype html>
|
| 123 |
-
<html lang="en">
|
| 124 |
-
<head>
|
| 125 |
-
<meta charset="utf-8" />
|
| 126 |
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 127 |
-
<title>HuggingMes Login</title>
|
| 128 |
<style>
|
| 129 |
-
:root
|
| 130 |
-
*
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
input
|
| 137 |
-
button
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
<body>
|
| 142 |
-
<
|
| 143 |
-
<h1>
|
| 144 |
-
<p>Enter
|
| 145 |
-
${errorHtml}
|
| 146 |
<form method="post" action="${LOGIN_PATH}">
|
| 147 |
<input type="hidden" name="next" value="${escapeHtml(safeNext)}" />
|
| 148 |
-
<
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
| 151 |
</form>
|
| 152 |
-
</
|
| 153 |
-
</body>
|
| 154 |
-
</html>`;
|
| 155 |
}
|
| 156 |
|
| 157 |
function escapeHtml(value) {
|
|
@@ -590,6 +584,7 @@ const server = http.createServer(async (req, res) => {
|
|
| 590 |
}
|
| 591 |
|
| 592 |
if (path === "/env-builder" || path === "/env-builder/") {
|
|
|
|
| 593 |
try {
|
| 594 |
const html = fs.readFileSync(require("path").join(__dirname, "env-builder.html"), "utf8");
|
| 595 |
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
@@ -602,6 +597,7 @@ const server = http.createServer(async (req, res) => {
|
|
| 602 |
}
|
| 603 |
|
| 604 |
if (path === "/env-builder.js") {
|
|
|
|
| 605 |
try {
|
| 606 |
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
|
| 607 |
res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
|
|
@@ -707,7 +703,11 @@ const server = http.createServer(async (req, res) => {
|
|
| 707 |
res.end("JupyterLab is not running. Set DEV_MODE=true and JUPYTER_TOKEN in Space secrets to enable /terminal/.");
|
| 708 |
return;
|
| 709 |
}
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
});
|
| 712 |
return;
|
| 713 |
}
|
|
|
|
| 116 |
|
| 117 |
function renderLoginPage(nextPath, errorMessage = "") {
|
| 118 |
const safeNext = sanitizeNext(nextPath);
|
| 119 |
+
return `<!doctype html><html lang="en"><head>
|
| 120 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
| 121 |
+
<title>HuggingMes</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
<style>
|
| 123 |
+
:root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--bad:#fb7185}
|
| 124 |
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);padding:24px}
|
| 125 |
+
.card{border:1px solid var(--line);background:var(--panel);border-radius:14px;padding:36px 32px;max-width:400px;width:100%;text-align:center}
|
| 126 |
+
h1{margin:0 0 8px;font-size:1.4rem}
|
| 127 |
+
.sub{color:var(--muted);font-size:.82rem;margin:0 0 24px}
|
| 128 |
+
.row{display:flex;gap:8px;margin-top:16px}
|
| 129 |
+
input{flex:1;background:#0d0c18;border:1px solid var(--line);border-radius:7px;padding:10px 12px;color:var(--text);font-size:.95rem;outline:none;transition:border-color .15s}
|
| 130 |
+
input:focus{border-color:#6366f1}
|
| 131 |
+
button{background:#fff;color:#000;border:none;border-radius:7px;padding:10px 20px;font-weight:700;font-size:.95rem;cursor:pointer;transition:opacity .15s;white-space:nowrap}
|
| 132 |
+
button:hover{opacity:.85}
|
| 133 |
+
.err{color:var(--bad);font-size:.82rem;margin-top:10px}
|
| 134 |
+
code{background:#232234;border:1px solid #34324c;border-radius:5px;padding:2px 6px;font-size:.88em}
|
| 135 |
+
</style></head><body>
|
| 136 |
+
<div class="card">
|
| 137 |
+
<h1>🪽 HuggingMes</h1>
|
| 138 |
+
<p class="sub">Enter your <code>GATEWAY_TOKEN</code> to continue</p>
|
|
|
|
| 139 |
<form method="post" action="${LOGIN_PATH}">
|
| 140 |
<input type="hidden" name="next" value="${escapeHtml(safeNext)}" />
|
| 141 |
+
<div class="row">
|
| 142 |
+
<input type="password" name="token" placeholder="GATEWAY_TOKEN" autofocus autocomplete="current-password" required>
|
| 143 |
+
<button type="submit">Unlock</button>
|
| 144 |
+
</div>
|
| 145 |
+
${errorMessage ? `<p class="err">Invalid token — try again</p>` : ""}
|
| 146 |
</form>
|
| 147 |
+
</div>
|
| 148 |
+
</body></html>`;
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
function escapeHtml(value) {
|
|
|
|
| 584 |
}
|
| 585 |
|
| 586 |
if (path === "/env-builder" || path === "/env-builder/") {
|
| 587 |
+
if (!requireAuth(req, res)) return;
|
| 588 |
try {
|
| 589 |
const html = fs.readFileSync(require("path").join(__dirname, "env-builder.html"), "utf8");
|
| 590 |
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
|
|
| 597 |
}
|
| 598 |
|
| 599 |
if (path === "/env-builder.js") {
|
| 600 |
+
if (!requireAuth(req, res)) return;
|
| 601 |
try {
|
| 602 |
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
|
| 603 |
res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
|
|
|
|
| 703 |
res.end("JupyterLab is not running. Set DEV_MODE=true and JUPYTER_TOKEN in Space secrets to enable /terminal/.");
|
| 704 |
return;
|
| 705 |
}
|
| 706 |
+
// Inject the Jupyter token so JupyterLab skips its own login screen.
|
| 707 |
+
// User already authenticated via GATEWAY_TOKEN — no second prompt needed.
|
| 708 |
+
const jToken = process.env.JUPYTER_TOKEN || "";
|
| 709 |
+
const overrides = jToken ? { authorization: `token ${jToken}` } : {};
|
| 710 |
+
proxyRequest(req, res, JUPYTER_PORT, (p) => p, overrides);
|
| 711 |
});
|
| 712 |
return;
|
| 713 |
}
|