Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions crates/httpd/src/auth_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,16 @@ pub async fn auth_gate(
next.run(request).await
} else {
// Remote connections to other pages when auth is not
// configured yet: redirect to a static "setup required"
// page instead of passing through, which would cause a
// redirect loop between `/` and `/onboarding` (#350).
Redirect::to("/setup-required").into_response()
// configured yet: send them to /onboarding so they can
// complete first-time setup via the setup-code flow
// (#350, #646). The original redirect loop between `/`
// and `/onboarding` was fixed separately at the SPA
// template layer via `should_redirect_from_onboarding`,
// which keeps remote visitors on /onboarding while auth
// setup is pending. The setup code (printed to stdout)
// still prevents an unauthorized remote visitor from
// claiming the instance.
Redirect::to("/onboarding").into_response()
}
},
AuthResult::Unauthorized => {
Expand Down
23 changes: 15 additions & 8 deletions crates/httpd/tests/auth_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1360,10 +1360,11 @@ async fn onboarding_passes_through_for_remote_during_setup() {
}

/// During setup (no password), a remote connection to / is redirected to
/// /setup-required (same as /onboarding).
/// /onboarding so the user can enter the setup code and complete first-
/// time setup via the wizard's AuthStep (#646).
#[cfg(feature = "web-ui")]
#[tokio::test]
async fn root_redirects_to_setup_required_for_remote() {
async fn root_redirects_to_onboarding_for_remote() {
let (addr, _store, _state) = start_proxied_server().await;

let client = reqwest::Client::builder()
Expand All @@ -1383,13 +1384,15 @@ async fn root_redirects_to_setup_required_for_remote() {
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert_eq!(
location, "/setup-required",
"remote / during setup must redirect to /setup-required"
location, "/onboarding",
"remote / during setup must redirect to /onboarding"
);
}

/// /setup-required is a public path and serves content even for remote
/// connections during setup (no redirect loop).
/// /setup-required is still served as a public stale-bookmark fallback
/// even for remote connections during setup. It is no longer the default
/// redirect target, but direct navigation must still work and must not
/// redirect-loop.
#[cfg(feature = "web-ui")]
#[tokio::test]
async fn setup_required_page_accessible_for_remote() {
Expand All @@ -1414,8 +1417,12 @@ async fn setup_required_page_accessible_for_remote() {
);
let body = resp.text().await.unwrap();
assert!(
body.contains("Authentication Not Configured"),
"/setup-required should contain the setup heading"
body.contains("First-time setup"),
"/setup-required should contain the new setup heading"
);
assert!(
body.contains("href=\"/onboarding\""),
"/setup-required should link to /onboarding"
);
}

Expand Down
12 changes: 8 additions & 4 deletions crates/web/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,12 +844,16 @@ mod tests {
"should produce a full HTML document"
);
assert!(
html.contains("Authentication Not Configured"),
"should contain the setup-required heading"
html.contains("First-time setup"),
"should contain the new setup heading"
);
assert!(
html.contains("moltis auth reset-password"),
"should contain the CLI reset command"
html.contains("setup code"),
"should mention the one-time setup code"
);
assert!(
html.contains("href=\"/onboarding\""),
"should link to the onboarding wizard"
);
assert!(
html.contains("/assets/v/test123/"),
Expand Down
16 changes: 10 additions & 6 deletions crates/web/src/templates/setup-required.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,22 @@
<link rel="stylesheet" href="{{ asset_prefix }}style.css">
<style>
body{display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
.card{max-width:460px;padding:2.5rem;border-radius:12px;border:1px solid var(--border);text-align:center}
.card{max-width:480px;padding:2.5rem;border-radius:12px;border:1px solid var(--border);text-align:center}
h1{margin:0 0 .75rem;font-size:1.35rem}
p{margin:.5rem 0;line-height:1.6;color:var(--muted)}
p{margin:.6rem 0;line-height:1.6;color:var(--muted)}
code{background:rgba(128,128,128,.15);padding:.15em .4em;border-radius:4px;font-size:.9em}
.cta{display:inline-block;margin-top:1.25rem;text-decoration:none}
.note{margin-top:1.5rem;font-size:.85em;opacity:.75}
</style>
</head>
<body>
<div class="card">
<h1>Authentication Not Configured</h1>
<p>This instance requires authentication to be set up before it can be accessed remotely.</p>
<p>Connect from the local machine or use the CLI:</p>
<p><code>moltis auth reset-password</code></p>
<h1>First-time setup</h1>
<p>This instance has not been configured yet. To finish setup remotely, use the one-time <strong>setup code</strong> printed to the server's standard output when the process started.</p>
<p>For a docker-compose deployment, find the code with:</p>
<p><code>docker compose logs moltis</code></p>
<a class="cta provider-btn provider-btn-secondary" href="/onboarding">Continue setup &rarr;</a>
<p class="note">Locked out of an existing instance with filesystem access? The <code>moltis auth reset-password</code> CLI command can recover it &mdash; do not use it on a fresh install.</p>
</div>
</body>
</html>
11 changes: 5 additions & 6 deletions crates/web/ui/e2e/specs/onboarding-auth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,11 @@ test.describe("Onboarding with forced auth (remote)", () => {

test("completes auth and identity steps via WebSocket", async ({ page }) => {
const pageErrors = watchPageErrors(page);
// Fresh runs should land on /onboarding (remote setup allows the
// onboarding page through for the setup-code auth flow). Retries
// can land on /login if a previous attempt already configured auth.
// Navigate directly to /onboarding since / redirects to
// /setup-required for remote connections during setup (#350).
await page.goto("/onboarding");
// Fresh runs visiting `/` on a remote (proxied) connection should
// be redirected to /onboarding so the setup-code AuthStep is
// shown (#350, #646). Retries can land on /login if a previous
// attempt already configured auth.
await page.goto("/");
await expect
.poll(() => new URL(page.url()).pathname, { timeout: 15_000 })
.toMatch(/^\/(?:onboarding|login|chats\/.+)$/);
Expand Down
Loading