fix(settings): require re-2FA confirmation for sensitive setting changes (#5610)

* fix(settings): require server-side 2fa for sensitive changes

* fix(lint): group third-party imports separately from local (goimports)

golangci-lint goimports flagged setting.go and setting_security_test.go because xlzd/gotp and gorm.io/gorm were mixed into the github.com/mhsanaei/3x-ui local-prefix group. Move them into the third-party group so the local imports stand alone.
This commit is contained in:
n0ctal
2026-06-28 18:17:15 +05:00
committed by GitHub
parent 25a86b9ee2
commit 2b10808fbd
7 changed files with 97 additions and 23 deletions
+21 -9
View File
@@ -37,6 +37,7 @@ interface ApiTokenRow {
interface SecurityTabProps {
allSetting: AllSetting;
updateSetting: (patch: Partial<AllSetting>) => void;
saveSetting: (payload: Partial<AllSetting> & Record<string, unknown>) => Promise<unknown>;
}
const UNIX_MILLISECONDS_THRESHOLD = 100_000_000_000;
@@ -65,7 +66,7 @@ const TFA_INITIAL: TfaState = {
onConfirm: () => {},
};
export default function SecurityTab({ allSetting, updateSetting }: SecurityTabProps) {
export default function SecurityTab({ allSetting, updateSetting, saveSetting }: SecurityTabProps) {
const { t } = useTranslation();
const { isMobile } = useMediaQuery();
const [modal, modalContextHolder] = Modal.useModal();
@@ -99,10 +100,10 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
setUser((prev) => ({ ...prev, [key]: value }));
}
const sendUpdateUser = useCallback(async () => {
const sendUpdateUser = useCallback(async (twoFactorCode = '') => {
setUpdating(true);
try {
const msg = await HttpUtil.post('/panel/api/setting/updateUser', user) as ApiMsg;
const msg = await HttpUtil.post('/panel/api/setting/updateUser', { ...user, twoFactorCode }) as ApiMsg;
if (msg?.success) {
await HttpUtil.post('/logout');
const basePath = window.X_UI_BASE_PATH || '/';
@@ -118,9 +119,11 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
openTfa({
title: t('pages.settings.security.twoFactorModalChangeCredentialsTitle'),
description: t('pages.settings.security.twoFactorModalChangeCredentialsStep'),
token: allSetting.twoFactorToken,
token: '',
type: 'confirm',
onConfirm: (ok: boolean) => { if (ok) sendUpdateUser(); },
onConfirm: (ok: boolean, code?: string) => {
if (ok) sendUpdateUser(code || '');
},
});
} else {
sendUpdateUser();
@@ -224,12 +227,21 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
openTfa({
title: t('pages.settings.security.twoFactorModalDeleteTitle'),
description: t('pages.settings.security.twoFactorModalRemoveStep'),
token: allSetting.twoFactorToken,
token: '',
type: 'confirm',
onConfirm: (ok: boolean) => {
onConfirm: async (ok: boolean, code?: string) => {
if (!ok) return;
messageApi.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
updateSetting({ twoFactorEnable: false, twoFactorToken: '' });
const next = {
...allSetting,
twoFactorEnable: false,
twoFactorToken: '',
twoFactorCode: code || '',
};
const msg = await saveSetting(next) as ApiMsg;
if (msg?.success) {
messageApi.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
updateSetting({ twoFactorEnable: false, twoFactorToken: '', hasTwoFactorToken: false });
}
},
});
}
+2 -1
View File
@@ -76,6 +76,7 @@ export default function SettingsPage() {
setSpinning,
saveDisabled,
saveAll,
savePayload,
} = useAllSettings();
const [entryHost, setEntryHost] = useState('');
@@ -196,7 +197,7 @@ export default function SettingsPage() {
const categoryBody = useMemo(() => {
switch (activeSlug) {
case 'security': return <SecurityTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'security': return <SecurityTab allSetting={allSetting} updateSetting={updateSetting} saveSetting={savePayload} />;
case 'telegram': return <TelegramTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'email': return <EmailTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'subscription': return <SubscriptionGeneralTab allSetting={allSetting} updateSetting={updateSetting} />;