Skip to content

Commit 109e245

Browse files
authored
feat(webapp): show the first and last characters of a new PAT (#4363)
1 parent bf41c5d commit 109e245

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
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.

apps/webapp/app/components/primitives/ClipboardField.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,43 @@ const variants = {
6060
},
6161
};
6262

63+
const SECURE_MASK = "••••••••••••••••";
64+
65+
/**
66+
* Builds the masked display string, optionally revealing the first/last few
67+
* characters in cleartext so users can confirm a copied value. A custom mask
68+
* string (when `secure` is a string) is always shown as-is.
69+
*/
70+
function maskValue(
71+
value: string,
72+
secure: boolean | string,
73+
revealStart: number,
74+
revealEnd: number
75+
) {
76+
if (typeof secure === "string") {
77+
return secure;
78+
}
79+
80+
const start = Math.max(0, revealStart);
81+
const end = Math.max(0, revealEnd);
82+
83+
// Nothing to reveal, or revealing would leak the whole value: fully mask.
84+
if ((start === 0 && end === 0) || start + end >= value.length) {
85+
return SECURE_MASK;
86+
}
87+
88+
const revealedStart = start > 0 ? value.slice(0, start) : "";
89+
const revealedEnd = end > 0 ? value.slice(-end) : "";
90+
return `${revealedStart}${SECURE_MASK}${revealedEnd}`;
91+
}
92+
6393
type ClipboardFieldProps = {
6494
value: string;
6595
secure?: boolean | string;
96+
/** When masked, reveal this many of the first characters in cleartext. */
97+
secureRevealStart?: number;
98+
/** When masked, reveal this many of the last characters in cleartext. */
99+
secureRevealEnd?: number;
66100
variant: keyof typeof variants;
67101
className?: string;
68102
icon?: React.ReactNode;
@@ -73,6 +107,8 @@ type ClipboardFieldProps = {
73107
export function ClipboardField({
74108
value,
75109
secure = false,
110+
secureRevealStart = 0,
111+
secureRevealEnd = 0,
76112
variant,
77113
className,
78114
icon,
@@ -87,6 +123,8 @@ export function ClipboardField({
87123
setIsSecure(secure !== undefined && secure);
88124
}, [secure]);
89125

126+
const maskedValue = maskValue(value, secure, secureRevealStart, secureRevealEnd);
127+
90128
return (
91129
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
92130
{icon && (
@@ -100,7 +138,7 @@ export function ClipboardField({
100138
<input
101139
type="text"
102140
ref={inputIcon}
103-
value={isSecure ? (typeof secure === "string" ? secure : "••••••••••••••••") : value}
141+
value={isSecure ? maskedValue : value}
104142
readOnly={true}
105143
className={cn(
106144
"shrink grow select-all overflow-x-auto",

apps/webapp/app/routes/account.tokens/route.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ function CreatePersonalAccessToken({
372372
</Callout>
373373
<ClipboardField
374374
secure
375+
// 7-char "tr_pat_" prefix + 4 token chars, matching the tokens list display
376+
secureRevealStart={11}
377+
secureRevealEnd={4}
375378
value={token.token}
376379
variant={"secondary/medium"}
377380
icon={<ShieldExclamationIcon className="size-5 text-success" />}

0 commit comments

Comments
 (0)