diff --git a/.server-changes/pat-token-reveal-last-chars.md b/.server-changes/pat-token-reveal-last-chars.md new file mode 100644 index 00000000000..29797f73d34 --- /dev/null +++ b/.server-changes/pat-token-reveal-last-chars.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +When you create a Personal Access Token, the generated token now shows its first and last few characters instead of being fully hidden, so you can confirm you copied the right value. diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 0304e5bca6b..7774f301218 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -60,9 +60,43 @@ const variants = { }, }; +const SECURE_MASK = "••••••••••••••••"; + +/** + * Builds the masked display string, optionally revealing the first/last few + * characters in cleartext so users can confirm a copied value. A custom mask + * string (when `secure` is a string) is always shown as-is. + */ +function maskValue( + value: string, + secure: boolean | string, + revealStart: number, + revealEnd: number +) { + if (typeof secure === "string") { + return secure; + } + + const start = Math.max(0, revealStart); + const end = Math.max(0, revealEnd); + + // Nothing to reveal, or revealing would leak the whole value: fully mask. + if ((start === 0 && end === 0) || start + end >= value.length) { + return SECURE_MASK; + } + + const revealedStart = start > 0 ? value.slice(0, start) : ""; + const revealedEnd = end > 0 ? value.slice(-end) : ""; + return `${revealedStart}${SECURE_MASK}${revealedEnd}`; +} + type ClipboardFieldProps = { value: string; secure?: boolean | string; + /** When masked, reveal this many of the first characters in cleartext. */ + secureRevealStart?: number; + /** When masked, reveal this many of the last characters in cleartext. */ + secureRevealEnd?: number; variant: keyof typeof variants; className?: string; icon?: React.ReactNode; @@ -73,6 +107,8 @@ type ClipboardFieldProps = { export function ClipboardField({ value, secure = false, + secureRevealStart = 0, + secureRevealEnd = 0, variant, className, icon, @@ -87,6 +123,8 @@ export function ClipboardField({ setIsSecure(secure !== undefined && secure); }, [secure]); + const maskedValue = maskValue(value, secure, secureRevealStart, secureRevealEnd); + return ( {icon && ( @@ -100,7 +138,7 @@ export function ClipboardField({ }