mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add token gate page for unauthenticated access
Visiting localhost:5173 without ?token= now shows a clean token input page instead of a broken dashboard. Token is persisted in sessionStorage so refreshes work within the same browser session. Bump version to 1.3.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"source": "./understand-anything-plugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "understand-anything",
|
||||
"displayName": "Understand Anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@understand-anything/skill",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { validateGraph } from "@understand-anything/core/schema";
|
||||
import type { GraphIssue } from "@understand-anything/core/schema";
|
||||
import { useDashboardStore } from "./store";
|
||||
@@ -13,20 +13,57 @@ import PersonaSelector from "./components/PersonaSelector";
|
||||
import ProjectOverview from "./components/ProjectOverview";
|
||||
import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp";
|
||||
import WarningBanner from "./components/WarningBanner";
|
||||
import TokenGate from "./components/TokenGate";
|
||||
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||
import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts";
|
||||
import { ThemeProvider } from "./themes/index.ts";
|
||||
import { ThemePicker } from "./components/ThemePicker.tsx";
|
||||
import type { ThemeConfig } from "./themes/index.ts";
|
||||
|
||||
// Extract the access token from the URL so protected endpoints can be fetched.
|
||||
const ACCESS_TOKEN = new URLSearchParams(window.location.search).get("token");
|
||||
const SESSION_TOKEN_KEY = "understand-anything-token";
|
||||
|
||||
function tokenUrl(path: string): string {
|
||||
return ACCESS_TOKEN ? `${path}?token=${ACCESS_TOKEN}` : path;
|
||||
/**
|
||||
* Resolve the access token from the URL query string or sessionStorage.
|
||||
* If found in the URL, persist to sessionStorage and strip the param from the address bar.
|
||||
*/
|
||||
function resolveInitialToken(): string | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const urlToken = params.get("token");
|
||||
if (urlToken) {
|
||||
sessionStorage.setItem(SESSION_TOKEN_KEY, urlToken);
|
||||
// Clean the URL
|
||||
params.delete("token");
|
||||
const cleanSearch = params.toString();
|
||||
const newUrl =
|
||||
window.location.pathname + (cleanSearch ? `?${cleanSearch}` : "") + window.location.hash;
|
||||
window.history.replaceState(null, "", newUrl);
|
||||
return urlToken;
|
||||
}
|
||||
return sessionStorage.getItem(SESSION_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** Build a URL with the token query param appended. */
|
||||
function tokenUrl(path: string, token: string | null): string {
|
||||
return token ? `${path}?token=${encodeURIComponent(token)}` : path;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(resolveInitialToken);
|
||||
|
||||
const handleTokenValid = useCallback((token: string) => {
|
||||
sessionStorage.setItem(SESSION_TOKEN_KEY, token);
|
||||
setAccessToken(token);
|
||||
}, []);
|
||||
|
||||
// Show the token gate when no token is available
|
||||
if (accessToken === null) {
|
||||
return <TokenGate onTokenValid={handleTokenValid} />;
|
||||
}
|
||||
|
||||
return <Dashboard accessToken={accessToken} />;
|
||||
}
|
||||
|
||||
function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const setGraph = useDashboardStore((s) => s.setGraph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
@@ -41,7 +78,7 @@ function App() {
|
||||
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(tokenUrl("/meta.json"))
|
||||
fetch(tokenUrl("/meta.json", accessToken))
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((meta) => {
|
||||
if (meta?.theme) setMetaTheme(meta.theme);
|
||||
@@ -133,7 +170,7 @@ function App() {
|
||||
useKeyboardShortcuts(shortcuts);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(tokenUrl("/knowledge-graph.json"))
|
||||
fetch(tokenUrl("/knowledge-graph.json", accessToken))
|
||||
.then((res) => res.json())
|
||||
.then((data: unknown) => {
|
||||
const result = validateGraph(data);
|
||||
@@ -162,7 +199,7 @@ function App() {
|
||||
}, [setGraph]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(tokenUrl("/diff-overlay.json"))
|
||||
fetch(tokenUrl("/diff-overlay.json", accessToken))
|
||||
.then((res) => {
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface TokenGateProps {
|
||||
onTokenValid: (token: string) => void;
|
||||
}
|
||||
|
||||
export default function TokenGate({ onTokenValid }: TokenGateProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const token = input.trim();
|
||||
if (!token) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/knowledge-graph.json?token=${encodeURIComponent(token)}`);
|
||||
if (res.ok) {
|
||||
onTokenValid(token);
|
||||
} else if (res.status === 403) {
|
||||
setError("Invalid token. Please check and try again.");
|
||||
} else {
|
||||
setError(`Unexpected response (${res.status}). Is the dashboard server running?`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
`Could not reach the server: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-root noise-overlay">
|
||||
<div className="w-full max-w-md px-8 py-10 bg-surface border border-border-subtle rounded-lg shadow-2xl">
|
||||
{/* Heading */}
|
||||
<h1 className="font-serif text-2xl text-text-primary tracking-wide text-center mb-2">
|
||||
Access Token Required
|
||||
</h1>
|
||||
<p className="text-text-muted text-sm text-center mb-8">
|
||||
Paste the access token from your terminal. Look for the{" "}
|
||||
<span role="img" aria-label="key">🔑</span> line.
|
||||
</p>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
if (error) setError(null);
|
||||
}}
|
||||
placeholder="Paste token here..."
|
||||
autoFocus
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded text-text-primary placeholder:text-text-muted/50 font-mono text-sm focus:outline-none focus:border-accent transition-colors"
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !input.trim()}
|
||||
className="w-full py-3 bg-accent text-root font-semibold rounded transition-all hover:brightness-110 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Validating..." : "Continue"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user