feat(webapp): show the first and last characters of a new PAT (#4363)
This commit is contained in:
@@ -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.
|
||||
@@ -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 (
|
||||
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
|
||||
{icon && (
|
||||
@@ -100,7 +138,7 @@ export function ClipboardField({
|
||||
<input
|
||||
type="text"
|
||||
ref={inputIcon}
|
||||
value={isSecure ? (typeof secure === "string" ? secure : "••••••••••••••••") : value}
|
||||
value={isSecure ? maskedValue : value}
|
||||
readOnly={true}
|
||||
className={cn(
|
||||
"shrink grow select-all overflow-x-auto",
|
||||
|
||||
@@ -372,6 +372,9 @@ function CreatePersonalAccessToken({
|
||||
</Callout>
|
||||
<ClipboardField
|
||||
secure
|
||||
// 7-char "tr_pat_" prefix + 4 token chars, matching the tokens list display
|
||||
secureRevealStart={11}
|
||||
secureRevealEnd={4}
|
||||
value={token.token}
|
||||
variant={"secondary/medium"}
|
||||
icon={<ShieldExclamationIcon className="size-5 text-success" />}
|
||||
|
||||
Reference in New Issue
Block a user