fix(app): add masking of phones to settings

This commit is contained in:
Hampus Kraft 2026-02-21 15:46:29 +00:00
parent a129b162b7
commit 2db53689a1
No known key found for this signature in database
GPG Key ID: 6090864C465A454D
4 changed files with 78 additions and 6 deletions

View File

@ -37,6 +37,7 @@ const AccountSecurityTab: React.FC = observer(() => {
const {t} = useLingui(); const {t} = useLingui();
const user = UserStore.currentUser; const user = UserStore.currentUser;
const [showMaskedEmail, setShowMaskedEmail] = useState(false); const [showMaskedEmail, setShowMaskedEmail] = useState(false);
const [showMaskedPhone, setShowMaskedPhone] = useState(false);
const [passkeys, setPasskeys] = useState<Array<UserActionCreators.WebAuthnCredential>>([]); const [passkeys, setPasskeys] = useState<Array<UserActionCreators.WebAuthnCredential>>([]);
const [loadingPasskeys, setLoadingPasskeys] = useState(false); const [loadingPasskeys, setLoadingPasskeys] = useState(false);
const [enablingSmsMfa, setEnablingSmsMfa] = useState(false); const [enablingSmsMfa, setEnablingSmsMfa] = useState(false);
@ -100,9 +101,11 @@ const AccountSecurityTab: React.FC = observer(() => {
loadingPasskeys={loadingPasskeys} loadingPasskeys={loadingPasskeys}
enablingSmsMfa={enablingSmsMfa} enablingSmsMfa={enablingSmsMfa}
disablingSmsMfa={disablingSmsMfa} disablingSmsMfa={disablingSmsMfa}
showMaskedPhone={showMaskedPhone}
loadPasskeys={loadPasskeys} loadPasskeys={loadPasskeys}
setEnablingSmsMfa={setEnablingSmsMfa} setEnablingSmsMfa={setEnablingSmsMfa}
setDisablingSmsMfa={setDisablingSmsMfa} setDisablingSmsMfa={setDisablingSmsMfa}
setShowMaskedPhone={setShowMaskedPhone}
/> />
</SettingsSection> </SettingsSection>

View File

@ -36,6 +36,7 @@ export const AccountSecurityInlineTab = observer(() => {
const {t} = useLingui(); const {t} = useLingui();
const user = UserStore.currentUser; const user = UserStore.currentUser;
const [showMaskedEmail, setShowMaskedEmail] = useState(false); const [showMaskedEmail, setShowMaskedEmail] = useState(false);
const [showMaskedPhone, setShowMaskedPhone] = useState(false);
const [passkeys, setPasskeys] = useState<Array<UserActionCreators.WebAuthnCredential>>([]); const [passkeys, setPasskeys] = useState<Array<UserActionCreators.WebAuthnCredential>>([]);
const [loadingPasskeys, setLoadingPasskeys] = useState(false); const [loadingPasskeys, setLoadingPasskeys] = useState(false);
const [enablingSmsMfa, setEnablingSmsMfa] = useState(false); const [enablingSmsMfa, setEnablingSmsMfa] = useState(false);
@ -89,9 +90,11 @@ export const AccountSecurityInlineTab = observer(() => {
loadingPasskeys={loadingPasskeys} loadingPasskeys={loadingPasskeys}
enablingSmsMfa={enablingSmsMfa} enablingSmsMfa={enablingSmsMfa}
disablingSmsMfa={disablingSmsMfa} disablingSmsMfa={disablingSmsMfa}
showMaskedPhone={showMaskedPhone}
loadPasskeys={loadPasskeys} loadPasskeys={loadPasskeys}
setEnablingSmsMfa={setEnablingSmsMfa} setEnablingSmsMfa={setEnablingSmsMfa}
setDisablingSmsMfa={setDisablingSmsMfa} setDisablingSmsMfa={setDisablingSmsMfa}
setShowMaskedPhone={setShowMaskedPhone}
/> />
</SettingsSection> </SettingsSection>
<SettingsSection id="danger_zone" title={t`Danger Zone`}> <SettingsSection id="danger_zone" title={t`Danger Zone`}>

View File

@ -97,6 +97,48 @@
gap: 0.5rem; gap: 0.5rem;
} }
.phoneRow {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@media (min-width: 640px) {
.phoneRow {
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
}
.phoneText {
color: var(--text-primary-muted);
font-size: 0.875rem;
}
.phoneTextSelectable {
user-select: text;
-webkit-user-select: text;
}
.toggleButton {
margin-top: 0.1em;
text-align: left;
color: var(--text-link);
font-size: 0.875rem;
cursor: pointer;
}
.toggleButton:hover {
text-decoration: underline;
}
@media (min-width: 640px) {
.toggleButton {
text-align: center;
}
}
.claimButton { .claimButton {
align-self: flex-start; align-self: flex-start;
} }

View File

@ -41,6 +41,15 @@ import type React from 'react';
const logger = new Logger('SecurityTab'); const logger = new Logger('SecurityTab');
const maskPhone = (phone: string): string => {
if (phone.length <= 4) {
return phone.replace(/./g, '*');
}
const lastTwo = phone.slice(-2);
const masked = phone.slice(0, -2).replace(/\d/g, '*');
return `${masked}${lastTwo}`;
};
interface SecurityTabProps { interface SecurityTabProps {
user: UserRecord; user: UserRecord;
isClaimed: boolean; isClaimed: boolean;
@ -51,9 +60,11 @@ interface SecurityTabProps {
loadingPasskeys: boolean; loadingPasskeys: boolean;
enablingSmsMfa: boolean; enablingSmsMfa: boolean;
disablingSmsMfa: boolean; disablingSmsMfa: boolean;
showMaskedPhone: boolean;
loadPasskeys: () => Promise<void>; loadPasskeys: () => Promise<void>;
setEnablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>; setEnablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>;
setDisablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>; setDisablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>;
setShowMaskedPhone: (show: boolean) => void;
} }
export const SecurityTabContent: React.FC<SecurityTabProps> = observer( export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
@ -67,9 +78,11 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
loadingPasskeys, loadingPasskeys,
enablingSmsMfa, enablingSmsMfa,
disablingSmsMfa, disablingSmsMfa,
showMaskedPhone,
loadPasskeys, loadPasskeys,
setEnablingSmsMfa, setEnablingSmsMfa,
setDisablingSmsMfa, setDisablingSmsMfa,
setShowMaskedPhone,
}) => { }) => {
const {t, i18n} = useLingui(); const {t, i18n} = useLingui();
@ -344,13 +357,24 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
<div className={styles.label}> <div className={styles.label}>
<Trans>Phone Number</Trans> <Trans>Phone Number</Trans>
</div> </div>
<div className={styles.description}> {user.phone ? (
{user.phone ? ( <div className={styles.phoneRow}>
<Trans>Phone number added: {user.phone}</Trans> <span className={`${styles.phoneText} ${showMaskedPhone ? styles.phoneTextSelectable : ''}`}>
) : ( {showMaskedPhone ? user.phone : maskPhone(user.phone)}
</span>
<button
type="button"
className={styles.toggleButton}
onClick={() => setShowMaskedPhone(!showMaskedPhone)}
>
{showMaskedPhone ? t`Hide` : t`Reveal`}
</button>
</div>
) : (
<div className={styles.description}>
<Trans>Add a phone number to enable SMS two-factor authentication</Trans> <Trans>Add a phone number to enable SMS two-factor authentication</Trans>
)} </div>
</div> )}
</div> </div>
{user.phone ? ( {user.phone ? (
<Button <Button