- ui/src/App.tsx: Create/first company titles and descriptions → VOCAB.company
- ui/src/components/OnboardingWizard.tsx: 3 company display strings → VOCAB
- ui/src/components/Sidebar.tsx: 'Select company' fallback → VOCAB
- ui/src/pages/CliAuth.tsx: 'Requested company' label → VOCAB
- ui/src/pages/AgentDetail.tsx: company library string → VOCAB
- server/src/services/company-portability.ts: 'Imported Company' x2 → 'Imported Workspace'
- cli/src/commands/client/{issue,approval,agent,dashboard,activity}.ts: option descriptions → VOCAB
- cli/src/commands/worktree.ts: error message and option description → VOCAB
- server/src/index.ts: comment cleanup (actual value already 'Owner')
- server/src/services/company-export-readme.ts: comment cleanup (value already 'Project Manager')
185 lines
7 KiB
TypeScript
185 lines
7 KiB
TypeScript
import { useMemo } from "react";
|
|
import { VOCAB } from "@paperclipai/branding";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Link, useParams, useSearchParams } from "@/lib/router";
|
|
import { Button } from "@/components/ui/button";
|
|
import { accessApi } from "../api/access";
|
|
import { authApi } from "../api/auth";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
|
|
export function CliAuthPage() {
|
|
const queryClient = useQueryClient();
|
|
const params = useParams();
|
|
const [searchParams] = useSearchParams();
|
|
const challengeId = (params.id ?? "").trim();
|
|
const token = (searchParams.get("token") ?? "").trim();
|
|
const currentPath = useMemo(
|
|
() => `/cli-auth/${encodeURIComponent(challengeId)}${token ? `?token=${encodeURIComponent(token)}` : ""}`,
|
|
[challengeId, token],
|
|
);
|
|
|
|
const sessionQuery = useQuery({
|
|
queryKey: queryKeys.auth.session,
|
|
queryFn: () => authApi.getSession(),
|
|
retry: false,
|
|
});
|
|
const challengeQuery = useQuery({
|
|
queryKey: ["cli-auth-challenge", challengeId, token],
|
|
queryFn: () => accessApi.getCliAuthChallenge(challengeId, token),
|
|
enabled: challengeId.length > 0 && token.length > 0,
|
|
retry: false,
|
|
});
|
|
|
|
const approveMutation = useMutation({
|
|
mutationFn: () => accessApi.approveCliAuthChallenge(challengeId, token),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session });
|
|
await challengeQuery.refetch();
|
|
},
|
|
});
|
|
|
|
const cancelMutation = useMutation({
|
|
mutationFn: () => accessApi.cancelCliAuthChallenge(challengeId, token),
|
|
onSuccess: async () => {
|
|
await challengeQuery.refetch();
|
|
},
|
|
});
|
|
|
|
if (!challengeId || !token) {
|
|
return <div className="mx-auto max-w-xl py-10 text-sm text-destructive">Invalid CLI auth URL.</div>;
|
|
}
|
|
|
|
if (sessionQuery.isLoading || challengeQuery.isLoading) {
|
|
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading CLI auth challenge...</div>;
|
|
}
|
|
|
|
if (challengeQuery.error) {
|
|
return (
|
|
<div className="mx-auto max-w-xl py-10">
|
|
<div className="rounded-lg border border-border bg-card p-6">
|
|
<h1 className="text-lg font-semibold">CLI auth challenge unavailable</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
{challengeQuery.error instanceof Error ? challengeQuery.error.message : "Challenge is invalid or expired."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const challenge = challengeQuery.data;
|
|
if (!challenge) {
|
|
return <div className="mx-auto max-w-xl py-10 text-sm text-destructive">CLI auth challenge unavailable.</div>;
|
|
}
|
|
|
|
if (challenge.status === "approved") {
|
|
return (
|
|
<div className="mx-auto max-w-xl py-10">
|
|
<div className="rounded-lg border border-border bg-card p-6">
|
|
<h1 className="text-xl font-semibold">CLI access approved</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
The Paperclip CLI can now finish authentication on the requesting machine.
|
|
</p>
|
|
<p className="mt-4 text-sm text-muted-foreground">
|
|
Command: <span className="font-mono text-foreground">{challenge.command}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (challenge.status === "cancelled" || challenge.status === "expired") {
|
|
return (
|
|
<div className="mx-auto max-w-xl py-10">
|
|
<div className="rounded-lg border border-border bg-card p-6">
|
|
<h1 className="text-xl font-semibold">
|
|
{challenge.status === "expired" ? "CLI auth challenge expired" : "CLI auth challenge cancelled"}
|
|
</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Start the CLI auth flow again from your terminal to generate a new approval request.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (challenge.requiresSignIn || !sessionQuery.data) {
|
|
return (
|
|
<div className="mx-auto max-w-xl py-10">
|
|
<div className="rounded-lg border border-border bg-card p-6">
|
|
<h1 className="text-xl font-semibold">Sign in required</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Sign in or create an account, then return to this page to approve the CLI access request.
|
|
</p>
|
|
<Button asChild className="mt-4">
|
|
<Link to={`/auth?next=${encodeURIComponent(currentPath)}`}>Sign in / Create account</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-xl py-10">
|
|
<div className="rounded-lg border border-border bg-card p-6">
|
|
<h1 className="text-xl font-semibold">Approve {VOCAB.appName} CLI access</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
A local {VOCAB.appName} CLI process is requesting {VOCAB.board.toLowerCase()} access to this instance.
|
|
</p>
|
|
|
|
<div className="mt-5 space-y-3 text-sm">
|
|
<div>
|
|
<div className="text-muted-foreground">Command</div>
|
|
<div className="font-mono text-foreground">{challenge.command}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-muted-foreground">Client</div>
|
|
<div className="text-foreground">{challenge.clientName ?? "nexus cli"}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-muted-foreground">Requested access</div>
|
|
<div className="text-foreground">
|
|
{challenge.requestedAccess === "instance_admin_required" ? "Instance admin" : VOCAB.board}
|
|
</div>
|
|
</div>
|
|
{challenge.requestedCompanyName && (
|
|
<div>
|
|
<div className="text-muted-foreground">{`Requested ${VOCAB.company.toLowerCase()}`}</div>
|
|
<div className="text-foreground">{challenge.requestedCompanyName}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{(approveMutation.error || cancelMutation.error) && (
|
|
<p className="mt-4 text-sm text-destructive">
|
|
{(approveMutation.error ?? cancelMutation.error) instanceof Error
|
|
? ((approveMutation.error ?? cancelMutation.error) as Error).message
|
|
: "Failed to update CLI auth challenge"}
|
|
</p>
|
|
)}
|
|
|
|
{!challenge.canApprove && (
|
|
<p className="mt-4 text-sm text-destructive">
|
|
This challenge requires instance-admin access. Sign in with an instance admin account to approve it.
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-5 flex gap-3">
|
|
<Button
|
|
onClick={() => approveMutation.mutate()}
|
|
disabled={!challenge.canApprove || approveMutation.isPending || cancelMutation.isPending}
|
|
>
|
|
{approveMutation.isPending ? "Approving..." : "Approve CLI access"}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => cancelMutation.mutate()}
|
|
disabled={approveMutation.isPending || cancelMutation.isPending}
|
|
>
|
|
{cancelMutation.isPending ? "Cancelling..." : "Cancel"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|