From 571b4844d9ffdf27c3bfc8c650a9ed35cb442cef Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 11 Feb 2026 19:30:25 +0530 Subject: [PATCH] feat: update local authentication mode to require a non-placeholder token of at least 50 characters --- .env.example | 3 +- README.md | 5 +- backend/.env.example | 3 +- backend/app/core/auth.py | 7 +- backend/app/core/auth_mode.py | 12 ++ backend/app/core/config.py | 28 +++- backend/tests/conftest.py | 6 +- .../tests/test_authenticate_request_flow.py | 11 +- backend/tests/test_config_auth_mode.py | 70 +++++++++ backend/tests/test_local_auth_integration.py | 3 +- compose.yml | 2 +- docs/deployment/README.md | 5 +- docs/production/README.md | 2 +- frontend/src/auth/localAuth.ts | 4 +- frontend/src/auth/mode.ts | 4 + .../organisms/LocalAuthLogin.test.tsx | 115 +++++++++++++++ .../components/organisms/LocalAuthLogin.tsx | 134 ++++++++++++++---- frontend/src/proxy.ts | 3 +- 18 files changed, 363 insertions(+), 54 deletions(-) create mode 100644 backend/app/core/auth_mode.py create mode 100644 backend/tests/test_config_auth_mode.py create mode 100644 frontend/src/auth/mode.ts create mode 100644 frontend/src/components/organisms/LocalAuthLogin.test.tsx diff --git a/.env.example b/.env.example index 1d75556b..6823a9d0 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,8 @@ DB_AUTO_MIGRATE=true LOG_LEVEL=INFO REQUEST_LOG_SLOW_MS=1000 AUTH_MODE=local -LOCAL_AUTH_TOKEN=change-me +# REQUIRED when AUTH_MODE=local (must be non-placeholder and at least 50 chars). +LOCAL_AUTH_TOKEN= # --- frontend settings --- # REQUIRED: Public URL used by the browser to reach the API. diff --git a/README.md b/README.md index 24ebb1fa..11b4272a 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,15 @@ Mission Control supports two auth modes via `AUTH_MODE`: ```bash cp .env.example .env +# REQUIRED for local auth mode: +# set LOCAL_AUTH_TOKEN to a non-placeholder value with at least 50 characters. + # REQUIRED: the browser must be able to reach the backend. # NEXT_PUBLIC_API_URL must be reachable from the *browser* (host), not an internal Docker network name. # Missing/blank NEXT_PUBLIC_API_URL will break frontend API calls (e.g. Activity feed). # Auth defaults in .env.example are local mode. -# For production, set LOCAL_AUTH_TOKEN to a strong random value. +# For production, set LOCAL_AUTH_TOKEN to a random value with at least 50 characters. # For Clerk mode, set AUTH_MODE=clerk and provide Clerk keys. docker compose -f compose.yml --env-file .env up -d --build diff --git a/backend/.env.example b/backend/.env.example index 0cd2515b..8d3f3dec 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -10,7 +10,8 @@ BASE_URL= # Auth mode: clerk or local. AUTH_MODE=local -LOCAL_AUTH_TOKEN=change-me +# REQUIRED when AUTH_MODE=local (must be non-placeholder and at least 50 chars). +LOCAL_AUTH_TOKEN= # Clerk (auth only; used when AUTH_MODE=clerk) CLERK_SECRET_KEY= diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 4ac7f6a1..14decaf9 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -16,6 +16,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from pydantic import BaseModel, ValidationError from starlette.concurrency import run_in_threadpool +from app.core.auth_mode import AuthMode from app.core.config import settings from app.core.logging import get_logger from app.db import crud @@ -244,7 +245,7 @@ async def _fetch_clerk_profile(clerk_user_id: str) -> tuple[str | None, str | No async def delete_clerk_user(clerk_user_id: str) -> None: """Delete a Clerk user via the official Clerk SDK.""" - if settings.auth_mode != "clerk": + if settings.auth_mode != AuthMode.CLERK: return secret = settings.clerk_secret_key.strip() @@ -422,7 +423,7 @@ async def get_auth_context( session: AsyncSession = SESSION_DEP, ) -> AuthContext: """Resolve required authenticated user context for the configured auth mode.""" - if settings.auth_mode == "local": + if settings.auth_mode == AuthMode.LOCAL: local_auth = await _resolve_local_auth_context( request=request, session=session, @@ -466,7 +467,7 @@ async def get_auth_context_optional( """Resolve user context if available, otherwise return `None`.""" if request.headers.get("X-Agent-Token"): return None - if settings.auth_mode == "local": + if settings.auth_mode == AuthMode.LOCAL: return await _resolve_local_auth_context( request=request, session=session, diff --git a/backend/app/core/auth_mode.py b/backend/app/core/auth_mode.py new file mode 100644 index 00000000..c2687fb5 --- /dev/null +++ b/backend/app/core/auth_mode.py @@ -0,0 +1,12 @@ +"""Shared auth-mode enum values.""" + +from __future__ import annotations + +from enum import Enum + + +class AuthMode(str, Enum): + """Supported authentication modes for backend and frontend.""" + + CLERK = "clerk" + LOCAL = "local" diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 08482ed5..d857e39e 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -3,13 +3,24 @@ from __future__ import annotations from pathlib import Path -from typing import Literal, Self +from typing import Self from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +from app.core.auth_mode import AuthMode + BACKEND_ROOT = Path(__file__).resolve().parents[2] DEFAULT_ENV_FILE = BACKEND_ROOT / ".env" +LOCAL_AUTH_TOKEN_MIN_LENGTH = 50 +LOCAL_AUTH_TOKEN_PLACEHOLDERS = frozenset( + { + "change-me", + "changeme", + "replace-me", + "replace-with-strong-random-token", + }, +) class Settings(BaseSettings): @@ -27,7 +38,7 @@ class Settings(BaseSettings): database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/openclaw_agency" # Auth mode: "clerk" for Clerk JWT auth, "local" for shared bearer token auth. - auth_mode: Literal["clerk", "local"] + auth_mode: AuthMode local_auth_token: str = "" # Clerk auth (auth only; roles stored in DB) @@ -51,15 +62,20 @@ class Settings(BaseSettings): @model_validator(mode="after") def _defaults(self) -> Self: - if self.auth_mode == "clerk": + if self.auth_mode == AuthMode.CLERK: if not self.clerk_secret_key.strip(): raise ValueError( "CLERK_SECRET_KEY must be set and non-empty when AUTH_MODE=clerk.", ) - elif self.auth_mode == "local": - if not self.local_auth_token.strip(): + elif self.auth_mode == AuthMode.LOCAL: + token = self.local_auth_token.strip() + if ( + not token + or len(token) < LOCAL_AUTH_TOKEN_MIN_LENGTH + or token.lower() in LOCAL_AUTH_TOKEN_PLACEHOLDERS + ): raise ValueError( - "LOCAL_AUTH_TOKEN must be set and non-empty when AUTH_MODE=local.", + "LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local.", ) # In dev, default to applying Alembic migrations at startup to avoid # schema drift (e.g. missing newly-added columns). diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index db99bf63..e522fc29 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -10,6 +10,6 @@ if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) # Tests should fail fast if auth-mode wiring breaks, but still need deterministic -# defaults during import-time settings initialization. -os.environ.setdefault("AUTH_MODE", "local") -os.environ.setdefault("LOCAL_AUTH_TOKEN", "test-local-token") +# defaults during import-time settings initialization, regardless of shell env. +os.environ["AUTH_MODE"] = "local" +os.environ["LOCAL_AUTH_TOKEN"] = "test-local-token-0123456789-0123456789-0123456789x" diff --git a/backend/tests/test_authenticate_request_flow.py b/backend/tests/test_authenticate_request_flow.py index a85612fb..d0fcde68 100644 --- a/backend/tests/test_authenticate_request_flow.py +++ b/backend/tests/test_authenticate_request_flow.py @@ -9,6 +9,7 @@ import pytest from fastapi import HTTPException from app.core import auth +from app.core.auth_mode import AuthMode from app.models.users import User @@ -21,7 +22,7 @@ class _FakeSession: async def test_get_auth_context_raises_401_when_clerk_signed_out( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(auth.settings, "auth_mode", "clerk") + monkeypatch.setattr(auth.settings, "auth_mode", AuthMode.CLERK) monkeypatch.setattr(auth.settings, "clerk_secret_key", "sk_test_dummy") from clerk_backend_api.security.types import AuthStatus, RequestState @@ -45,7 +46,7 @@ async def test_get_auth_context_raises_401_when_clerk_signed_out( async def test_get_auth_context_uses_request_state_payload_claims( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(auth.settings, "auth_mode", "clerk") + monkeypatch.setattr(auth.settings, "auth_mode", AuthMode.CLERK) monkeypatch.setattr(auth.settings, "clerk_secret_key", "sk_test_dummy") from clerk_backend_api.security.types import AuthStatus, RequestState @@ -88,7 +89,7 @@ async def test_get_auth_context_uses_request_state_payload_claims( async def test_get_auth_context_optional_returns_none_for_agent_token( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(auth.settings, "auth_mode", "clerk") + monkeypatch.setattr(auth.settings, "auth_mode", AuthMode.CLERK) monkeypatch.setattr(auth.settings, "clerk_secret_key", "sk_test_dummy") async def _boom(_request: Any) -> Any: # pragma: no cover @@ -108,7 +109,7 @@ async def test_get_auth_context_optional_returns_none_for_agent_token( async def test_get_auth_context_local_mode_requires_valid_bearer_token( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(auth.settings, "auth_mode", "local") + monkeypatch.setattr(auth.settings, "auth_mode", AuthMode.LOCAL) monkeypatch.setattr(auth.settings, "local_auth_token", "expected-token") async def _fake_local_user(_session: Any) -> User: @@ -131,7 +132,7 @@ async def test_get_auth_context_local_mode_requires_valid_bearer_token( async def test_get_auth_context_optional_local_mode_returns_none_without_token( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(auth.settings, "auth_mode", "local") + monkeypatch.setattr(auth.settings, "auth_mode", AuthMode.LOCAL) monkeypatch.setattr(auth.settings, "local_auth_token", "expected-token") async def _boom(_session: Any) -> User: # pragma: no cover diff --git a/backend/tests/test_config_auth_mode.py b/backend/tests/test_config_auth_mode.py new file mode 100644 index 00000000..1f913302 --- /dev/null +++ b/backend/tests/test_config_auth_mode.py @@ -0,0 +1,70 @@ +# ruff: noqa: INP001 +"""Settings validation tests for auth-mode configuration.""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from app.core.auth_mode import AuthMode +from app.core.config import Settings + + +def test_local_mode_requires_non_empty_token() -> None: + with pytest.raises( + ValidationError, + match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.LOCAL, + local_auth_token="", + ) + + +def test_local_mode_requires_minimum_length() -> None: + with pytest.raises( + ValidationError, + match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.LOCAL, + local_auth_token="x" * 49, + ) + + +def test_local_mode_rejects_placeholder_token() -> None: + with pytest.raises( + ValidationError, + match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.LOCAL, + local_auth_token="change-me", + ) + + +def test_local_mode_accepts_real_token() -> None: + token = "a" * 50 + settings = Settings( + _env_file=None, + auth_mode=AuthMode.LOCAL, + local_auth_token=token, + ) + + assert settings.auth_mode == AuthMode.LOCAL + assert settings.local_auth_token == token + + +def test_clerk_mode_requires_secret_key() -> None: + with pytest.raises( + ValidationError, + match="CLERK_SECRET_KEY must be set and non-empty when AUTH_MODE=clerk", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.CLERK, + clerk_secret_key="", + ) diff --git a/backend/tests/test_local_auth_integration.py b/backend/tests/test_local_auth_integration.py index 135afdff..79e30f6b 100644 --- a/backend/tests/test_local_auth_integration.py +++ b/backend/tests/test_local_auth_integration.py @@ -14,6 +14,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession from app.api.users import router as users_router from app.core import auth as auth_module +from app.core.auth_mode import AuthMode from app.core.config import settings from app.db.session import get_session @@ -51,7 +52,7 @@ async def test_local_auth_users_me_requires_and_accepts_valid_token( expected_email = f"local-{unique_suffix}@localhost" expected_name = "Local Integration User" - monkeypatch.setattr(settings, "auth_mode", "local") + monkeypatch.setattr(settings, "auth_mode", AuthMode.LOCAL) monkeypatch.setattr(settings, "local_auth_token", "integration-token") monkeypatch.setattr(auth_module, "LOCAL_AUTH_USER_ID", expected_user_id) monkeypatch.setattr(auth_module, "LOCAL_AUTH_EMAIL", expected_email) diff --git a/compose.yml b/compose.yml index 30ee9318..f3c85fd1 100644 --- a/compose.yml +++ b/compose.yml @@ -31,7 +31,7 @@ services: CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000} DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true} AUTH_MODE: ${AUTH_MODE} - LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN:-change-me} + LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN} depends_on: db: condition: service_healthy diff --git a/docs/deployment/README.md b/docs/deployment/README.md index ef844497..3423ab26 100644 --- a/docs/deployment/README.md +++ b/docs/deployment/README.md @@ -32,6 +32,9 @@ From repo root: ```bash cp .env.example .env +# REQUIRED for local mode: +# set LOCAL_AUTH_TOKEN in .env to a non-placeholder value with at least 50 characters. + docker compose -f compose.yml --env-file .env up -d --build ``` @@ -125,7 +128,7 @@ Set in `.env` (repo root): ```env AUTH_MODE=local -LOCAL_AUTH_TOKEN=replace-with-strong-random-token +LOCAL_AUTH_TOKEN=replace-with-random-token-at-least-50-characters ``` Set frontend mode (optional override in `frontend/.env`): diff --git a/docs/production/README.md b/docs/production/README.md index ac50ff17..98f1db4b 100644 --- a/docs/production/README.md +++ b/docs/production/README.md @@ -60,7 +60,7 @@ Recommended approach: Secrets guidelines: - Choose auth mode explicitly: - - `AUTH_MODE=local`: set a strong `LOCAL_AUTH_TOKEN` + - `AUTH_MODE=local`: set `LOCAL_AUTH_TOKEN` to a random value with at least 50 characters - `AUTH_MODE=clerk`: configure Clerk keys - Never commit `LOCAL_AUTH_TOKEN` or Clerk secret key. - Prefer passing secrets as environment variables from the host (or use Docker secrets if you later diff --git a/frontend/src/auth/localAuth.ts b/frontend/src/auth/localAuth.ts index caba1f9c..cfe59c97 100644 --- a/frontend/src/auth/localAuth.ts +++ b/frontend/src/auth/localAuth.ts @@ -1,10 +1,12 @@ "use client"; +import { AuthMode } from "@/auth/mode"; + let localToken: string | null = null; const STORAGE_KEY = "mc_local_auth_token"; export function isLocalAuthMode(): boolean { - return process.env.NEXT_PUBLIC_AUTH_MODE === "local"; + return process.env.NEXT_PUBLIC_AUTH_MODE === AuthMode.Local; } export function setLocalAuthToken(token: string): void { diff --git a/frontend/src/auth/mode.ts b/frontend/src/auth/mode.ts new file mode 100644 index 00000000..6bf1d992 --- /dev/null +++ b/frontend/src/auth/mode.ts @@ -0,0 +1,4 @@ +export enum AuthMode { + Clerk = "clerk", + Local = "local", +} diff --git a/frontend/src/components/organisms/LocalAuthLogin.test.tsx b/frontend/src/components/organisms/LocalAuthLogin.test.tsx new file mode 100644 index 00000000..49675f8f --- /dev/null +++ b/frontend/src/components/organisms/LocalAuthLogin.test.tsx @@ -0,0 +1,115 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { LocalAuthLogin } from "./LocalAuthLogin"; + +const setLocalAuthTokenMock = vi.hoisted(() => vi.fn()); +const fetchMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/auth/localAuth", async () => { + const actual = await vi.importActual( + "@/auth/localAuth", + ); + return { + ...actual, + setLocalAuthToken: setLocalAuthTokenMock, + }; +}); + +describe("LocalAuthLogin", () => { + beforeEach(() => { + fetchMock.mockReset(); + setLocalAuthTokenMock.mockReset(); + vi.stubGlobal("fetch", fetchMock); + vi.stubEnv("NEXT_PUBLIC_API_URL", "http://localhost:8000/"); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it("requires a non-empty token", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: "Continue" })); + + expect(screen.getByText("Bearer token is required.")).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(setLocalAuthTokenMock).not.toHaveBeenCalled(); + }); + + it("requires token length of at least 50 characters", async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByPlaceholderText("Paste token"), "x".repeat(49)); + await user.click(screen.getByRole("button", { name: "Continue" })); + + expect( + screen.getByText("Bearer token must be at least 50 characters."), + ).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(setLocalAuthTokenMock).not.toHaveBeenCalled(); + }); + + it("rejects invalid token values", async () => { + const onAuthenticatedMock = vi.fn(); + fetchMock.mockResolvedValueOnce(new Response(null, { status: 401 })); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByPlaceholderText("Paste token"), "x".repeat(50)); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => + expect(screen.getByText("Token is invalid.")).toBeInTheDocument(), + ); + expect(fetchMock).toHaveBeenCalledWith( + "http://localhost:8000/api/v1/users/me", + expect.objectContaining({ + method: "GET", + headers: { Authorization: `Bearer ${"x".repeat(50)}` }, + }), + ); + expect(setLocalAuthTokenMock).not.toHaveBeenCalled(); + expect(onAuthenticatedMock).not.toHaveBeenCalled(); + }); + + it("saves token only after successful backend validation", async () => { + const onAuthenticatedMock = vi.fn(); + fetchMock.mockResolvedValueOnce(new Response(null, { status: 200 })); + const user = userEvent.setup(); + render(); + + const token = ` ${"g".repeat(50)} `; + await user.type(screen.getByPlaceholderText("Paste token"), token); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => + expect(setLocalAuthTokenMock).toHaveBeenCalledWith("g".repeat(50)), + ); + expect(onAuthenticatedMock).toHaveBeenCalledTimes(1); + }); + + it("shows a clear error when backend is unreachable", async () => { + const onAuthenticatedMock = vi.fn(); + fetchMock.mockRejectedValueOnce(new TypeError("network error")); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByPlaceholderText("Paste token"), "t".repeat(50)); + await user.click(screen.getByRole("button", { name: "Continue" })); + + await waitFor(() => + expect( + screen.getByText("Unable to reach backend to validate token."), + ).toBeInTheDocument(), + ); + expect(setLocalAuthTokenMock).not.toHaveBeenCalled(); + expect(onAuthenticatedMock).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/organisms/LocalAuthLogin.tsx b/frontend/src/components/organisms/LocalAuthLogin.tsx index b071d593..0ecccf4e 100644 --- a/frontend/src/components/organisms/LocalAuthLogin.tsx +++ b/frontend/src/components/organisms/LocalAuthLogin.tsx @@ -8,54 +8,132 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -export function LocalAuthLogin() { +const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50; + +async function validateLocalToken(token: string): Promise { + const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; + if (!rawBaseUrl) { + return "NEXT_PUBLIC_API_URL is not set."; + } + + const baseUrl = rawBaseUrl.replace(/\/+$/, ""); + + let response: Response; + try { + response = await fetch(`${baseUrl}/api/v1/users/me`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + } catch { + return "Unable to reach backend to validate token."; + } + + if (response.ok) { + return null; + } + if (response.status === 401 || response.status === 403) { + return "Token is invalid."; + } + return `Unable to validate token (HTTP ${response.status}).`; +} + +type LocalAuthLoginProps = { + onAuthenticated?: () => void; +}; + +const defaultOnAuthenticated = () => window.location.reload(); + +export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) { const [token, setToken] = useState(""); const [error, setError] = useState(null); + const [isValidating, setIsValidating] = useState(false); - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const cleaned = token.trim(); if (!cleaned) { setError("Bearer token is required."); return; } + if (cleaned.length < LOCAL_AUTH_TOKEN_MIN_LENGTH) { + setError( + `Bearer token must be at least ${LOCAL_AUTH_TOKEN_MIN_LENGTH} characters.`, + ); + return; + } + + setIsValidating(true); + const validationError = await validateLocalToken(cleaned); + setIsValidating(false); + if (validationError) { + setError(validationError); + return; + } + setLocalAuthToken(cleaned); setError(null); - window.location.reload(); + (onAuthenticated ?? defaultOnAuthenticated)(); }; return ( -
- - -
- +
+
+
+
+
+ + + +
+ + Self-host mode + +
+ +
-
-

+
+

Local Authentication

-

- Enter the shared local token configured as - - LOCAL_AUTH_TOKEN - - on the backend. +

+ Enter your access token to unlock Mission Control.

- -
- setToken(event.target.value)} - placeholder="Paste token" - autoFocus - /> - {error ?

{error}

: null} -
diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts index f636dfec..0052d4ba 100644 --- a/frontend/src/proxy.ts +++ b/frontend/src/proxy.ts @@ -2,9 +2,10 @@ import { NextResponse } from "next/server"; import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; import { isLikelyValidClerkPublishableKey } from "@/auth/clerkKey"; +import { AuthMode } from "@/auth/mode"; const isClerkEnabled = () => - process.env.NEXT_PUBLIC_AUTH_MODE !== "local" && + process.env.NEXT_PUBLIC_AUTH_MODE !== AuthMode.Local && isLikelyValidClerkPublishableKey( process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, );