diff --git a/src/features/jwt-decoder/JwtDecoderPage.tsx b/src/features/jwt-decoder/JwtDecoderPage.tsx index c701e92..3755267 100644 --- a/src/features/jwt-decoder/JwtDecoderPage.tsx +++ b/src/features/jwt-decoder/JwtDecoderPage.tsx @@ -1,65 +1,198 @@ -import type { JSX } from "react"; -import { useMemo, useState } from "react"; -import { Copy, RotateCcw, ShieldCheck } from "lucide-react"; +import type { JSX, ReactNode } from "react"; +import { useEffect, useMemo, useState } from "react"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { + Check, + CheckCircle2, + ChevronDown, + Clock3, + Copy, + Download, + FileJson2, + Fingerprint, + KeyRound, + RotateCcw, + ShieldAlert, + ShieldCheck, + Signature, + Sparkles, + TriangleAlert, + WrapText, + XCircle, +} from "lucide-react"; +import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Tooltip } from "@/shared/ui/tooltip"; +import { PaneHeader, ToolSurface, ToolToolbar } from "@/shared/components/ToolSurface"; import { - PaneHeader, - ToolOutput, - ToolSurface, - ToolTextarea, - ToolToolbar, - ToolTitle, -} from "@/shared/components/ToolSurface"; -import { decodeJwt } from "./jwt-decoder.service"; + decodeJwt, + generateJwtExample, + getJwtBreakdownRows, + jwtAlgorithms, + verifyJwtSignature, + type DecodedJwt, + type JwtAlgorithm, + type JwtBreakdownRow, + type JwtClaimInsight, + type JwtClaimStatus, + type JwtSecurityWarning, + type JwtVerificationResult, +} from "./jwt-decoder.service"; + +type DecoderView = "decoded" | "claims" | "verify"; +type DecodedSectionView = "breakdown" | "json"; + +const sampleToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvcmdlLWRldi1rZXkifQ.eyJzdWIiOiJ1c2VyXzQyIiwibmFtZSI6IkZvcmdlIERldmVsb3BlciIsImFkbWluIjpmYWxzZSwiaXNzIjoiaHR0cHM6Ly9hdXRoLmZvcmdlLmxvY2FsIiwiYXVkIjpbImZvcmdlLWFwaSIsImZvcmdlLWNsaSJdLCJpYXQiOjE3MTk4MDAwMDAsIm5iZiI6MTcxOTgwMDAwMCwiZXhwIjoyNTMyNDAwMDAwLCJzY29wZSI6InRvb2xzOnJlYWQgdG9vbHM6d3JpdGUiLCJyb2xlcyI6WyJkZXZlbG9wZXIiLCJyZXZpZXdlciJdLCJtZXRhIjp7IndvcmtzcGFjZSI6Im9yY2FjZSIsImxvY2FsT25seSI6dHJ1ZX19.7LZ-tyRUyyrqelMiTMJxfvxwKHUXn-ViflCiJLCL-4A"; export function JwtDecoderPage(): JSX.Element { - const [token, setToken] = useState(""); + const [token, setToken] = useState(sampleToken); + const [view, setView] = useState("decoded"); + const [secret, setSecret] = useState("forge-secret"); + const [lineWrap, setLineWrap] = useState(true); + const [exampleAlgorithm, setExampleAlgorithm] = useState("HS256"); + const [verification, setVerification] = useState({ + message: "Enter a shared secret to verify HMAC signatures.", + status: "idle", + }); const decoded = useMemo(() => decodeJwt(token), [token]); - async function copyPayload(): Promise { - await navigator.clipboard.writeText(decoded.payload); + useEffect(() => { + let cancelled = false; + + void verifyJwtSignature(token, secret).then((result) => { + if (!cancelled) { + setVerification(result); + } + }); + + return () => { + cancelled = true; + }; + }, [secret, token]); + + async function copyToken(): Promise { + await navigator.clipboard.writeText(token); + } + + async function copyDecoded(): Promise { + await navigator.clipboard.writeText( + JSON.stringify( + { + header: decoded.headerJson, + payload: decoded.payloadJson, + signature: decoded.signature, + }, + null, + 2, + ), + ); + } + + function downloadDecoded(): void { + const blob = new Blob( + [ + JSON.stringify( + { + header: decoded.headerJson, + payload: decoded.payloadJson, + signature: decoded.signature, + }, + null, + 2, + ), + ], + { type: "application/json;charset=utf-8" }, + ); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + + anchor.href = url; + anchor.download = "forge-jwt-decoded.json"; + anchor.click(); + URL.revokeObjectURL(url); + } + + async function generateExample(algorithm: JwtAlgorithm): Promise { + setExampleAlgorithm(algorithm); + setToken(await generateJwtExample(algorithm, secret)); + setView("decoded"); } return ( - -

-

- +
+
+ } + title="Encoded token" + tone="blue" /> + -
- -
-
-                {decoded.header || "Header"}
-              
-
-                {decoded.payload || "Payload"}
-              
-

- {decoded.signature || "Signature"} -

-
-
+ +
+ } + title={} + tone={decoded.error ? "rose" : "emerald"} + /> + {decoded.error ? ( + + ) : view === "claims" ? ( + + ) : view === "verify" ? ( + + ) : ( + + )}
); } + +interface ExampleControlProps { + onGenerate: (algorithm: JwtAlgorithm) => void; + value: JwtAlgorithm; +} + +function ExampleControl({ onGenerate, value }: ExampleControlProps): JSX.Element { + return ( + + + + + + + {jwtAlgorithms.map((algorithm) => ( + onGenerate(algorithm)} + > + {algorithm} + {value === algorithm ? ( + + ))} + + + + ); +} + +interface ViewButtonProps { + active: boolean; + icon: ReactNode; + label: string; + onClick: () => void; +} + +function ViewButton({ active, icon, label, onClick }: ViewButtonProps): JSX.Element { + return ( + + ); +} + +function StatusPill({ + decoded, + verification, +}: { + decoded: DecodedJwt; + verification: JwtVerificationResult; +}): JSX.Element { + if (decoded.error) { + return ( + + + ); + } + + if (!decoded.payload) { + return ( + + + ); + } + + if (verification.status === "verified") { + return ( + + + ); + } + + return ( + + + ); +} + +function TokenInput({ + lineWrap, + onChange, + value, +}: { + lineWrap: boolean; + onChange: (value: string) => void; + value: string; +}): JSX.Element { + const segments = value.split("."); + + return ( +
+ +