Mission Control with OpenClaw hook - added simple API and updated configs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AuthMode {
|
||||
Clerk = "clerk",
|
||||
Local = "local",
|
||||
Disabled = "disabled",
|
||||
}
|
||||
|
||||
@@ -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}</>;
|
||||
|
||||
Reference in New Issue
Block a user