mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
feat: add ClawHub docs auth broker
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
transfersGetRouterV1Http,
|
||||
usersListV1Http,
|
||||
usersPostRouterV1Http,
|
||||
verifyDocsSessionV1Http,
|
||||
whoamiV1Http,
|
||||
} from "./httpApiV1";
|
||||
import { preflightHandler } from "./httpPreflight";
|
||||
@@ -188,6 +189,12 @@ http.route({
|
||||
handler: whoamiV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
path: "/api/v1/docs/session/verify",
|
||||
method: "GET",
|
||||
handler: verifyDocsSessionV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
pathPrefix: `${ApiRoutes.users}/`,
|
||||
method: "POST",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { httpAction } from "./functions";
|
||||
import { verifyDocsSessionV1Handler } from "./httpApiV1/docsSessionV1";
|
||||
import {
|
||||
listBundlePluginsV1Handler,
|
||||
listCodePluginsV1Handler,
|
||||
@@ -44,6 +45,7 @@ export const mintPublishTokenV1Http = httpAction(mintPublishTokenV1Handler);
|
||||
export const npmMirrorGetHttp = httpAction(npmMirrorGetHandler);
|
||||
export const listCodePluginsV1Http = httpAction(listCodePluginsV1Handler);
|
||||
export const listBundlePluginsV1Http = httpAction(listBundlePluginsV1Handler);
|
||||
export const verifyDocsSessionV1Http = httpAction(verifyDocsSessionV1Handler);
|
||||
|
||||
export const searchSkillsV1Http = httpAction(searchSkillsV1Handler);
|
||||
export const resolveSkillVersionV1Http = httpAction(resolveSkillVersionV1Handler);
|
||||
@@ -79,6 +81,7 @@ export const __handlers = {
|
||||
npmMirrorGetHandler,
|
||||
listCodePluginsV1Handler,
|
||||
listBundlePluginsV1Handler,
|
||||
verifyDocsSessionV1Handler,
|
||||
searchSkillsV1Handler,
|
||||
resolveSkillVersionV1Handler,
|
||||
listSkillsV1Handler,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { internal } from "../_generated/api";
|
||||
import type { ActionCtx } from "../_generated/server";
|
||||
import { getOptionalActiveAuthUserIdFromAction } from "../lib/access";
|
||||
import { applyRateLimit } from "../lib/httpRateLimit";
|
||||
import { json, text } from "./shared";
|
||||
|
||||
export async function verifyDocsSessionV1Handler(ctx: ActionCtx, request: Request) {
|
||||
const rate = await applyRateLimit(ctx, request, "read");
|
||||
if (!rate.ok) return rate.response;
|
||||
|
||||
try {
|
||||
const userId = await getOptionalActiveAuthUserIdFromAction(ctx);
|
||||
if (!userId) return text("Unauthorized", 401, rate.headers);
|
||||
const user = await ctx.runQuery(internal.users.getByIdInternal, { userId });
|
||||
if (!user || user.deletedAt || user.deactivatedAt) {
|
||||
return text("Unauthorized", 401, rate.headers);
|
||||
}
|
||||
return json(
|
||||
{
|
||||
provider: "github",
|
||||
user: {
|
||||
id: user._id,
|
||||
handle: user.handle ?? user.name ?? null,
|
||||
displayName: user.displayName ?? null,
|
||||
image: user.image ?? null,
|
||||
},
|
||||
},
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
} catch {
|
||||
return text("Unauthorized", 401, rate.headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDocsAuthCallbackUrl, normalizeDocsReturnTo } from "./docsAuth";
|
||||
|
||||
describe("docs auth helpers", () => {
|
||||
it("allows documentation return URLs and rejects unrelated origins", () => {
|
||||
expect(normalizeDocsReturnTo("https://documentation.openclaw.ai/concepts/models")).toBe(
|
||||
"https://documentation.openclaw.ai/concepts/models",
|
||||
);
|
||||
expect(normalizeDocsReturnTo("https://docs.openclaw.ai/install")).toBe(
|
||||
"https://docs.openclaw.ai/install",
|
||||
);
|
||||
expect(normalizeDocsReturnTo("https://example.com/docs")).toBeNull();
|
||||
expect(normalizeDocsReturnTo("javascript:alert(1)")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the production docs callback for public docs hosts", () => {
|
||||
expect(buildDocsAuthCallbackUrl("https://documentation.openclaw.ai/concepts/models")).toBe(
|
||||
"https://documentation.openclaw.ai/ask-molty/auth/callback",
|
||||
);
|
||||
expect(buildDocsAuthCallbackUrl("https://docs.openclaw.ai/concepts/models")).toBe(
|
||||
"https://documentation.openclaw.ai/ask-molty/auth/callback",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps local callbacks local for dev", () => {
|
||||
expect(buildDocsAuthCallbackUrl("http://localhost:4173/start")).toBe(
|
||||
"http://localhost:4173/ask-molty/auth/callback",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
const allowedDocsOrigins = new Set([
|
||||
"https://documentation.openclaw.ai",
|
||||
"https://docs.openclaw.ai",
|
||||
"http://localhost:4173",
|
||||
"http://127.0.0.1:4173",
|
||||
]);
|
||||
|
||||
const productionDocsOrigin = "https://documentation.openclaw.ai";
|
||||
|
||||
export function normalizeDocsReturnTo(value?: string | null) {
|
||||
if (!value) return null;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (!allowedDocsOrigins.has(url.origin)) return null;
|
||||
if (!["http:", "https:"].includes(url.protocol)) return null;
|
||||
return url.href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDocsAuthCallbackUrl(returnTo: string) {
|
||||
const normalized = normalizeDocsReturnTo(returnTo);
|
||||
if (!normalized) return null;
|
||||
const url = new URL(normalized);
|
||||
const callbackOrigin =
|
||||
url.hostname === "localhost" || url.hostname === "127.0.0.1"
|
||||
? url.origin
|
||||
: productionDocsOrigin;
|
||||
return `${callbackOrigin}/ask-molty/auth/callback`;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getClawHubSiteUrl,
|
||||
getOnlyCrabsHost,
|
||||
getOnlyCrabsSiteUrl,
|
||||
isClawHubHost,
|
||||
getSiteDescription,
|
||||
getSiteMode,
|
||||
getSiteName,
|
||||
@@ -88,6 +89,14 @@ describe("site helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts both ClawHub domains as ClawHub hosts", () => {
|
||||
expect(isClawHubHost("clawhub.ai")).toBe(true);
|
||||
expect(isClawHubHost("www.clawhub.ai")).toBe(true);
|
||||
expect(isClawHubHost("hub.openclaw.ai")).toBe(true);
|
||||
expect(isClawHubHost("clawdhub.com")).toBe(false);
|
||||
expect(isClawHubHost("example.com")).toBe(false);
|
||||
});
|
||||
|
||||
it("detects site mode from window when available", () => {
|
||||
withServerEnv({ VITE_SOULHUB_HOST: "onlycrabs.ai" }, () => {
|
||||
vi.stubGlobal("window", { location: { hostname: "onlycrabs.ai" } } as unknown as Window);
|
||||
|
||||
@@ -6,6 +6,7 @@ const DEFAULT_CLAWHUB_SITE_URL = "https://clawhub.ai";
|
||||
const DEFAULT_ONLYCRABS_SITE_URL = "https://onlycrabs.ai";
|
||||
const DEFAULT_ONLYCRABS_HOST = "onlycrabs.ai";
|
||||
const LEGACY_CLAWDHUB_HOSTS = new Set(["clawdhub.com", "www.clawdhub.com", "auth.clawdhub.com"]);
|
||||
const OPENCLAW_CLAWHUB_HOSTS = new Set(["hub.openclaw.ai"]);
|
||||
|
||||
export function normalizeClawHubSiteOrigin(value?: string | null) {
|
||||
if (!value) return null;
|
||||
@@ -20,6 +21,16 @@ export function normalizeClawHubSiteOrigin(value?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isClawHubHost(host?: string | null) {
|
||||
if (!host) return false;
|
||||
const normalized = host.toLowerCase();
|
||||
return (
|
||||
normalized === "clawhub.ai" ||
|
||||
normalized === "www.clawhub.ai" ||
|
||||
OPENCLAW_CLAWHUB_HOSTS.has(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
export function getClawHubSiteUrl() {
|
||||
return normalizeClawHubSiteOrigin(getRuntimeEnv("VITE_SITE_URL")) ?? DEFAULT_CLAWHUB_SITE_URL;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Route as PluginsNameRouteImport } from './routes/plugins/$name'
|
||||
import { Route as PackagesNewRouteImport } from './routes/packages/new'
|
||||
import { Route as PackagesNameRouteImport } from './routes/packages/$name'
|
||||
import { Route as OrgsHandleRouteImport } from './routes/orgs/$handle'
|
||||
import { Route as DocsAuthRouteImport } from './routes/docs/auth'
|
||||
import { Route as CliAuthRouteImport } from './routes/cli/auth'
|
||||
import { Route as OwnerSlugRouteImport } from './routes/$owner/$slug'
|
||||
import { Route as PluginsScopeNameRouteImport } from './routes/plugins/$scope/$name'
|
||||
@@ -168,6 +169,11 @@ const OrgsHandleRoute = OrgsHandleRouteImport.update({
|
||||
path: '/orgs/$handle',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DocsAuthRoute = DocsAuthRouteImport.update({
|
||||
id: '/docs/auth',
|
||||
path: '/docs/auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const CliAuthRoute = CliAuthRouteImport.update({
|
||||
id: '/cli/auth',
|
||||
path: '/cli/auth',
|
||||
@@ -228,6 +234,7 @@ export interface FileRoutesByFullPath {
|
||||
'/upload': typeof UploadRoute
|
||||
'/$owner/$slug': typeof OwnerSlugRouteWithChildren
|
||||
'/cli/auth': typeof CliAuthRoute
|
||||
'/docs/auth': typeof DocsAuthRoute
|
||||
'/orgs/$handle': typeof OrgsHandleRoute
|
||||
'/packages/$name': typeof PackagesNameRoute
|
||||
'/packages/new': typeof PackagesNewRoute
|
||||
@@ -263,6 +270,7 @@ export interface FileRoutesByTo {
|
||||
'/upload': typeof UploadRoute
|
||||
'/$owner/$slug': typeof OwnerSlugRouteWithChildren
|
||||
'/cli/auth': typeof CliAuthRoute
|
||||
'/docs/auth': typeof DocsAuthRoute
|
||||
'/orgs/$handle': typeof OrgsHandleRoute
|
||||
'/packages/$name': typeof PackagesNameRoute
|
||||
'/packages/new': typeof PackagesNewRoute
|
||||
@@ -299,6 +307,7 @@ export interface FileRoutesById {
|
||||
'/upload': typeof UploadRoute
|
||||
'/$owner/$slug': typeof OwnerSlugRouteWithChildren
|
||||
'/cli/auth': typeof CliAuthRoute
|
||||
'/docs/auth': typeof DocsAuthRoute
|
||||
'/orgs/$handle': typeof OrgsHandleRoute
|
||||
'/packages/$name': typeof PackagesNameRoute
|
||||
'/packages/new': typeof PackagesNewRoute
|
||||
@@ -336,6 +345,7 @@ export interface FileRouteTypes {
|
||||
| '/upload'
|
||||
| '/$owner/$slug'
|
||||
| '/cli/auth'
|
||||
| '/docs/auth'
|
||||
| '/orgs/$handle'
|
||||
| '/packages/$name'
|
||||
| '/packages/new'
|
||||
@@ -371,6 +381,7 @@ export interface FileRouteTypes {
|
||||
| '/upload'
|
||||
| '/$owner/$slug'
|
||||
| '/cli/auth'
|
||||
| '/docs/auth'
|
||||
| '/orgs/$handle'
|
||||
| '/packages/$name'
|
||||
| '/packages/new'
|
||||
@@ -406,6 +417,7 @@ export interface FileRouteTypes {
|
||||
| '/upload'
|
||||
| '/$owner/$slug'
|
||||
| '/cli/auth'
|
||||
| '/docs/auth'
|
||||
| '/orgs/$handle'
|
||||
| '/packages/$name'
|
||||
| '/packages/new'
|
||||
@@ -442,6 +454,7 @@ export interface RootRouteChildren {
|
||||
UploadRoute: typeof UploadRoute
|
||||
OwnerSlugRoute: typeof OwnerSlugRouteWithChildren
|
||||
CliAuthRoute: typeof CliAuthRoute
|
||||
DocsAuthRoute: typeof DocsAuthRoute
|
||||
OrgsHandleRoute: typeof OrgsHandleRoute
|
||||
PackagesNameRoute: typeof PackagesNameRoute
|
||||
PackagesNewRoute: typeof PackagesNewRoute
|
||||
@@ -635,6 +648,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof OrgsHandleRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs/auth': {
|
||||
id: '/docs/auth'
|
||||
path: '/docs/auth'
|
||||
fullPath: '/docs/auth'
|
||||
preLoaderRoute: typeof DocsAuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/cli/auth': {
|
||||
id: '/cli/auth'
|
||||
path: '/cli/auth'
|
||||
@@ -747,6 +767,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
UploadRoute: UploadRoute,
|
||||
OwnerSlugRoute: OwnerSlugRouteWithChildren,
|
||||
CliAuthRoute: CliAuthRoute,
|
||||
DocsAuthRoute: DocsAuthRoute,
|
||||
OrgsHandleRoute: OrgsHandleRoute,
|
||||
PackagesNameRoute: PackagesNameRoute,
|
||||
PackagesNewRoute: PackagesNewRoute,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let mockSearch: { return_to?: string } = {};
|
||||
let mockAuthToken: string | null = "convex.jwt";
|
||||
let mockAuthStatus = {
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
me: { _id: "user_123" } as { _id: string } | null,
|
||||
};
|
||||
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
createFileRoute: () => (config: { component: unknown }) => ({
|
||||
...config,
|
||||
useSearch: () => mockSearch,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@convex-dev/auth/react", () => ({
|
||||
useAuthToken: () => mockAuthToken,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/useAuthStatus", () => ({
|
||||
useAuthStatus: () => mockAuthStatus,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/useAuthError", () => ({
|
||||
useAuthError: () => ({ error: null, clear: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("../../components/layout/Container", () => ({
|
||||
Container: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../../components/SignInButton", () => ({
|
||||
SignInButton: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../components/ui/button", () => ({
|
||||
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../components/ui/card", () => ({
|
||||
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CardContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CardHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CardTitle: ({ children }: { children: React.ReactNode }) => <h1>{children}</h1>,
|
||||
}));
|
||||
|
||||
const { DocsAuth } = await import("./auth");
|
||||
|
||||
describe("DocsAuth", () => {
|
||||
beforeEach(() => {
|
||||
mockSearch = { return_to: "https://documentation.openclaw.ai/concepts/models" };
|
||||
mockAuthToken = "convex.jwt";
|
||||
mockAuthStatus = {
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
me: { _id: "user_123" },
|
||||
};
|
||||
});
|
||||
|
||||
it("posts the ClawHub auth token to the docs callback", () => {
|
||||
render(<DocsAuth autoSubmit={false} />);
|
||||
|
||||
const form = screen.getByRole("button", { name: /continue to docs/i }).closest("form");
|
||||
expect(form?.getAttribute("method")).toBe("post");
|
||||
expect(form?.getAttribute("action")).toBe(
|
||||
"https://documentation.openclaw.ai/ask-molty/auth/callback",
|
||||
);
|
||||
expect(document.querySelector<HTMLInputElement>('input[name="token"]')?.value).toBe(
|
||||
"convex.jwt",
|
||||
);
|
||||
expect(document.querySelector<HTMLInputElement>('input[name="return_to"]')?.value).toBe(
|
||||
"https://documentation.openclaw.ai/concepts/models",
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for GitHub verification when no ClawHub session exists", () => {
|
||||
mockAuthStatus = { isAuthenticated: false, isLoading: false, me: null };
|
||||
|
||||
render(<DocsAuth autoSubmit={false} />);
|
||||
|
||||
expect(screen.getByRole("heading", { name: /verify with github/i })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: /verify with github/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("rejects unsafe return URLs", () => {
|
||||
mockSearch = { return_to: "https://example.com/steal" };
|
||||
|
||||
render(<DocsAuth autoSubmit={false} />);
|
||||
|
||||
expect(screen.getByText(/invalid docs return url/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useAuthToken } from "@convex-dev/auth/react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Container } from "../../components/layout/Container";
|
||||
import { SignInButton } from "../../components/SignInButton";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
|
||||
import { buildDocsAuthCallbackUrl, normalizeDocsReturnTo } from "../../lib/docsAuth";
|
||||
import { getClawHubSiteUrl, normalizeClawHubSiteOrigin } from "../../lib/site";
|
||||
import { useAuthError } from "../../lib/useAuthError";
|
||||
import { useAuthStatus } from "../../lib/useAuthStatus";
|
||||
|
||||
export const Route = createFileRoute("/docs/auth")({
|
||||
component: DocsAuth,
|
||||
});
|
||||
|
||||
type DocsAuthProps = {
|
||||
autoSubmit?: boolean;
|
||||
};
|
||||
|
||||
export function DocsAuth({ autoSubmit = true }: DocsAuthProps = {}) {
|
||||
const { isAuthenticated, isLoading, me } = useAuthStatus();
|
||||
const authToken = useAuthToken();
|
||||
const { error: authError, clear: clearAuthError } = useAuthError();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const submittedRef = useRef(false);
|
||||
const search = Route.useSearch() as { return_to?: string };
|
||||
const returnTo = normalizeDocsReturnTo(search.return_to);
|
||||
const callbackUrl = returnTo ? buildDocsAuthCallbackUrl(returnTo) : null;
|
||||
const registry = useMemo(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return normalizeClawHubSiteOrigin(window.location.origin) ?? getClawHubSiteUrl();
|
||||
}
|
||||
return getClawHubSiteUrl();
|
||||
}, []);
|
||||
|
||||
const canReturn = Boolean(returnTo && callbackUrl);
|
||||
const ready = canReturn && isAuthenticated && me && authToken;
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSubmit || !ready || submittedRef.current) return;
|
||||
submittedRef.current = true;
|
||||
formRef.current?.submit();
|
||||
}, [autoSubmit, ready]);
|
||||
|
||||
if (!canReturn) {
|
||||
return (
|
||||
<AuthFrame title="Docs agent login">
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">Invalid docs return URL.</p>
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
Open Ask Molty from the OpenClaw documentation page and try again.
|
||||
</p>
|
||||
</AuthFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated || !me) {
|
||||
return (
|
||||
<AuthFrame title="Verify with GitHub">
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
Sign in to ClawHub with GitHub to unlock Ask Molty on the OpenClaw docs.
|
||||
</p>
|
||||
{authError ? (
|
||||
<p
|
||||
className="rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300"
|
||||
role="alert"
|
||||
>
|
||||
{authError}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAuthError}
|
||||
aria-label="Dismiss"
|
||||
className="cursor-pointer border-none bg-transparent px-0.5 text-inherit"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</p>
|
||||
) : null}
|
||||
<SignInButton disabled={isLoading}>Verify with GitHub</SignInButton>
|
||||
</AuthFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authToken) {
|
||||
return (
|
||||
<AuthFrame title="Connecting docs">
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">Preparing your ClawHub session.</p>
|
||||
</AuthFrame>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFrame title="Connecting docs">
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
Returning to the OpenClaw docs with your ClawHub login.
|
||||
</p>
|
||||
<form ref={formRef} method="post" action={callbackUrl ?? undefined}>
|
||||
<input type="hidden" name="token" value={authToken} />
|
||||
<input type="hidden" name="return_to" value={returnTo ?? ""} />
|
||||
<input type="hidden" name="registry" value={registry} />
|
||||
<Button type="submit" variant="primary">
|
||||
Continue to docs
|
||||
</Button>
|
||||
</form>
|
||||
</AuthFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthFrame({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="py-10">
|
||||
<Container size="narrow">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"headers": [
|
||||
{
|
||||
"key": "Content-Security-Policy",
|
||||
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests"
|
||||
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self' https://documentation.openclaw.ai http://localhost:4173 http://127.0.0.1:4173; upgrade-insecure-requests"
|
||||
},
|
||||
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "key": "X-Frame-Options", "value": "DENY" },
|
||||
|
||||
Reference in New Issue
Block a user