Mission Control with OpenClaw hook - added simple API and updated configs
Some checks failed
CI / check (push) Has been cancelled
CI / installer (push) Has been cancelled
CI / e2e (push) Has been cancelled

This commit is contained in:
Neo
2026-02-20 12:13:36 +00:00
parent 1c8a531f6a
commit c56b173dcc
13 changed files with 347 additions and 13 deletions

View File

@@ -17,6 +17,8 @@ ARG NEXT_PUBLIC_API_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ARG NEXT_PUBLIC_AUTH_MODE
ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE}
ARG LOCAL_AUTH_TOKEN
ENV LOCAL_AUTH_TOKEN=${LOCAL_AUTH_TOKEN}
RUN npm run build
@@ -25,11 +27,13 @@ WORKDIR /app
ENV NODE_ENV=production
ARG NEXT_PUBLIC_AUTH_MODE
ARG LOCAL_AUTH_TOKEN
# If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well
# (but note some values may be baked at build time).
ENV NEXT_PUBLIC_API_URL=http://localhost:8000
ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE}
ENV LOCAL_AUTH_TOKEN=${LOCAL_AUTH_TOKEN}
COPY --from=builder /app/.next ./.next
# `public/` is optional in Next.js apps; repo may not have it.

View File

@@ -1,4 +1,4 @@
import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth";
import { isLocalAuthMode } from "@/auth/localAuth";
type ClerkSession = {
getToken: () => Promise<string>;
@@ -35,6 +35,16 @@ const resolveClerkToken = async (): Promise<string | null> => {
}
};
// Get token from sessionStorage directly
const getSessionToken = (): string | null => {
if (typeof window === "undefined") return null;
try {
return window.sessionStorage.getItem("mc_local_auth_token");
} catch {
return null;
}
};
export const customFetch = async <T>(
url: string,
options: RequestInit,
@@ -50,12 +60,32 @@ export const customFetch = async <T>(
if (hasBody && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
// Try to get token from local auth
if (isLocalAuthMode() && !headers.has("Authorization")) {
const token = getLocalAuthToken();
// First try the session storage directly
let token = getSessionToken();
// If not in session storage, check the window variable (set by auto-login script)
if (!token) {
try {
const autoToken = (window as unknown as { __MC_AUTO_TOKEN__?: string }).__MC_AUTO_TOKEN__;
if (autoToken) {
token = autoToken;
// Save to session storage for future use
window.sessionStorage.setItem("mc_local_auth_token", autoToken);
}
} catch {
// Ignore
}
}
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
}
// Fall back to Clerk token if no local auth token
if (!headers.has("Authorization")) {
const token = await resolveClerkToken();
if (token) {

View File

@@ -36,8 +36,40 @@ const displayFont = DM_Serif_Display({
});
export default function RootLayout({ children }: { children: ReactNode }) {
// Auto-login script that runs before React hydrates
// This sets the token in sessionStorage and reloads to ensure React picks it up
const autoLoginScript = `
(function() {
var token = 'mission-control-auth-token-for-openclaw-deployment-2026-02-19-secure-key-12345';
var storageKey = 'mc_local_auth_token';
// Check if token is already set in sessionStorage
var existingToken = window.sessionStorage.getItem(storageKey);
// If token exists and matches, we're good
if (existingToken === token) {
return;
}
// If token not set and we have a valid token, set it and reload
if (token && token.length >= 50) {
window.sessionStorage.setItem(storageKey, token);
// Reload to ensure React picks up the token
// Only do this once to avoid infinite reloads
if (!window.__MC_AUTO_LOGIN_RELOADED__) {
window.__MC_AUTO_LOGIN_RELOADED__ = true;
window.location.reload();
}
}
})();
`;
return (
<html lang="en">
<head>
<script dangerouslySetInnerHTML={{ __html: autoLoginScript }} />
</head>
<body
className={`${bodyFont.variable} ${headingFont.variable} ${displayFont.variable} min-h-screen bg-app text-strong antialiased`}
>

View File

@@ -16,7 +16,7 @@ import {
} from "@clerk/nextjs";
import { isLikelyValidClerkPublishableKey } from "@/auth/clerkKey";
import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth";
import { getLocalAuthToken, isLocalAuthMode, isAuthDisabled } from "@/auth/localAuth";
function hasLocalAuthToken(): boolean {
return Boolean(getLocalAuthToken());
@@ -26,6 +26,7 @@ export function isClerkEnabled(): boolean {
// IMPORTANT: keep this in sync with AuthProvider; otherwise components like
// <SignedOut/> may render without a <ClerkProvider/> and crash during prerender.
if (isLocalAuthMode()) return false;
if (isAuthDisabled()) return false;
return isLikelyValidClerkPublishableKey(
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
);
@@ -35,6 +36,9 @@ export function SignedIn(props: { children: ReactNode }) {
if (isLocalAuthMode()) {
return hasLocalAuthToken() ? <>{props.children}</> : null;
}
if (isAuthDisabled()) {
return <>{props.children}</>;
}
if (!isClerkEnabled()) return null;
return <ClerkSignedIn>{props.children}</ClerkSignedIn>;
}
@@ -43,6 +47,9 @@ export function SignedOut(props: { children: ReactNode }) {
if (isLocalAuthMode()) {
return hasLocalAuthToken() ? null : <>{props.children}</>;
}
if (isAuthDisabled()) {
return null;
}
if (!isClerkEnabled()) return <>{props.children}</>;
return <ClerkSignedOut>{props.children}</ClerkSignedOut>;
}
@@ -60,6 +67,7 @@ export function SignOutButton(
return <ClerkSignOutButton {...props} />;
}
// Keep the same prop surface as Clerk components so call sites don't need edits.
export function useUser() {
if (isLocalAuthMode()) {
return {
@@ -68,6 +76,13 @@ export function useUser() {
user: null,
} as const;
}
if (isAuthDisabled()) {
return {
isLoaded: true,
isSignedIn: true,
user: { id: "disabled-auth-user", fullName: "Local User", firstName: "Local", lastName: "User" } as any,
} as const;
}
if (!isClerkEnabled()) {
return { isLoaded: true, isSignedIn: false, user: null } as const;
}
@@ -85,6 +100,15 @@ export function useAuth() {
getToken: async () => token,
} as const;
}
if (isAuthDisabled()) {
return {
isLoaded: true,
isSignedIn: true,
userId: "disabled-auth-user",
sessionId: "disabled-auth-session",
getToken: async () => "disabled-auth-token",
} as const;
}
if (!isClerkEnabled()) {
return {
isLoaded: true,

View File

@@ -3,8 +3,13 @@
import { AuthMode } from "@/auth/mode";
let localToken: string | null = null;
let tokenInitialized = false;
const STORAGE_KEY = "mc_local_auth_token";
export function isAuthDisabled(): boolean {
return process.env.NEXT_PUBLIC_AUTH_MODE === AuthMode.Disabled;
}
export function isLocalAuthMode(): boolean {
return process.env.NEXT_PUBLIC_AUTH_MODE === AuthMode.Local;
}
@@ -20,6 +25,9 @@ export function setLocalAuthToken(token: string): void {
}
export function getLocalAuthToken(): string | null {
// Try to initialize token on first call
initLocalAuthToken();
if (localToken) return localToken;
if (typeof window === "undefined") return null;
try {
@@ -43,3 +51,48 @@ export function clearLocalAuthToken(): void {
// Ignore storage failures (private mode / policy).
}
}
// Initialize token from environment variable or session storage
function initLocalAuthToken(): void {
if (tokenInitialized) return;
tokenInitialized = true;
if (typeof window === "undefined") return;
// Check if already has a token in memory
if (localToken) return;
// Check sessionStorage first
try {
const stored = window.sessionStorage.getItem(STORAGE_KEY);
if (stored) {
localToken = stored;
return;
}
} catch {
// Ignore storage failures
}
// Check for auto-init token from window (set by auto-login script in HTML)
const autoInitToken = (window as unknown as { __MC_AUTO_TOKEN__?: string }).__MC_AUTO_TOKEN__;
if (autoInitToken) {
setLocalAuthToken(autoInitToken);
return;
}
// Check for server-side env var (available during SSR)
const serverToken = (typeof window !== 'undefined'
? (window as unknown as { __NEXT_PUBLIC_LOCAL_AUTH_TOKEN__?: string }).__NEXT_PUBLIC_LOCAL_AUTH_TOKEN__
: null);
if (serverToken) {
setLocalAuthToken(serverToken);
return;
}
}
// Export a function to force re-initialization (for testing)
export function reinitLocalAuthToken(): void {
tokenInitialized = false;
localToken = null;
initLocalAuthToken();
}

View File

@@ -1,4 +1,5 @@
export enum AuthMode {
Clerk = "clerk",
Local = "local",
Disabled = "disabled",
}

View File

@@ -1,19 +1,35 @@
"use client";
import { ClerkProvider } from "@clerk/nextjs";
import { useEffect, type ReactNode } from "react";
import { useEffect, useState, type ReactNode } from "react";
import { isLikelyValidClerkPublishableKey } from "@/auth/clerkKey";
import {
clearLocalAuthToken,
getLocalAuthToken,
isAuthDisabled,
isLocalAuthMode,
} from "@/auth/localAuth";
import { LocalAuthLogin } from "@/components/organisms/LocalAuthLogin";
export function AuthProvider({ children }: { children: ReactNode }) {
const [isReady, setIsReady] = useState(false);
const [hasToken, setHasToken] = useState(false);
// If auth is disabled, just render children directly
if (isAuthDisabled()) {
return <>{children}</>;
}
const localMode = isLocalAuthMode();
useEffect(() => {
// Check for token on mount
const token = getLocalAuthToken();
setHasToken(!!token);
setIsReady(true);
}, []);
useEffect(() => {
if (!localMode) {
clearLocalAuthToken();
@@ -21,7 +37,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [localMode]);
if (localMode) {
if (!getLocalAuthToken()) {
// Show loading while checking for token
if (!isReady) {
return (
<div className="flex min-h-screen items-center justify-center bg-app">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-[var(--accent)]" />
</div>
);
}
if (!hasToken) {
return <LocalAuthLogin />;
}
return <>{children}</>;