feat(devices): add functionality to delete all authorized devices

This commit is contained in:
shuaiplus
2026-03-08 22:12:01 +08:00
parent 1062725b46
commit c34c44ce5b
10 changed files with 230 additions and 41 deletions
+25 -1
View File
@@ -43,6 +43,7 @@ import {
getPreloginKdfConfig,
getProfile,
getAuthorizedDevices,
getCurrentDeviceIdentifier,
getSetupStatus,
getSends,
getTotpStatus,
@@ -60,6 +61,7 @@ import {
saveSession,
setTotp,
setUserStatus,
deleteAllAuthorizedDevices,
deleteAuthorizedDevice,
uploadCipherAttachment,
updateCipher,
@@ -969,10 +971,21 @@ export default function App() {
async function removeDeviceAction(device: AuthorizedDevice) {
await deleteAuthorizedDevice(authedFetch, device.identifier);
if (device.identifier === getCurrentDeviceIdentifier()) {
pushToast('success', t('txt_device_removed'));
logoutNow();
return;
}
await authorizedDevicesQuery.refetch();
pushToast('success', t('txt_device_removed'));
}
async function removeAllDevicesAction() {
await deleteAllAuthorizedDevices(authedFetch);
pushToast('success', t('txt_all_devices_removed'));
logoutNow();
}
async function createVaultItem(draft: VaultDraft, attachments: File[] = []) {
if (!session) return;
try {
@@ -2004,7 +2017,7 @@ export default function App() {
onRemoveDevice={(device) => {
setConfirm({
title: t('txt_remove_device'),
message: t('txt_remove_device_name_and_clear_its_2fa_trust', { name: device.name }),
message: t('txt_remove_device_and_sign_out_name', { name: device.name }),
danger: true,
onConfirm: () => {
setConfirm(null);
@@ -2023,6 +2036,17 @@ export default function App() {
},
});
}}
onRemoveAll={() => {
setConfirm({
title: t('txt_remove_all_devices'),
message: t('txt_remove_all_devices_and_sign_out_all_sessions'),
danger: true,
onConfirm: () => {
setConfirm(null);
void removeAllDevicesAction();
},
});
}}
/>
</div>
</Route>
@@ -9,6 +9,7 @@ interface SecurityDevicesPageProps {
onRevokeTrust: (device: AuthorizedDevice) => void;
onRemoveDevice: (device: AuthorizedDevice) => void;
onRevokeAll: () => void;
onRemoveAll: () => void;
}
function formatDateTime(value: string | null | undefined): string {
@@ -47,7 +48,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
<div>
<h3 style={{ margin: 0 }}>{t('txt_device_management')}</h3>
<div className="muted-inline" style={{ marginTop: 4 }}>
{t('txt_manage_authorized_devices_and_30_day_totp_trusted_sessions')}
{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}
</div>
</div>
<div className="actions">
@@ -59,6 +60,10 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
<ShieldOff size={14} className="btn-icon" />
{t('txt_revoke_all_trusted')}
</button>
<button type="button" className="btn btn-danger small" onClick={props.onRemoveAll}>
<Trash2 size={14} className="btn-icon" />
{t('txt_remove_all_devices')}
</button>
</div>
</div>
</section>
+11
View File
@@ -119,6 +119,10 @@ function getOrCreateDeviceIdentifier(): string {
return next;
}
export function getCurrentDeviceIdentifier(): string {
return (localStorage.getItem(DEVICE_IDENTIFIER_KEY) || '').trim();
}
function guessDeviceName(): string {
const ua = (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase();
const platform = (typeof navigator !== 'undefined' ? navigator.platform : '').trim();
@@ -772,6 +776,13 @@ export async function deleteAuthorizedDevice(
if (!resp.ok) throw new Error('Failed to remove device');
}
export async function deleteAllAuthorizedDevices(
authedFetch: (input: string, init?: RequestInit) => Promise<Response>
): Promise<void> {
const resp = await authedFetch('/api/devices', { method: 'DELETE' });
if (!resp.ok) throw new Error('Failed to remove all devices');
}
export async function listAdminUsers(authedFetch: (input: string, init?: RequestInit) => Promise<Response>): Promise<AdminUser[]> {
const resp = await authedFetch('/api/admin/users');
if (!resp.ok) throw new Error('Failed to load users');
+12
View File
@@ -232,6 +232,7 @@ const messages: Record<Locale, Record<string, string>> = {
txt_login_success: "Login success",
txt_macos_desktop: "macOS Desktop",
txt_manage_authorized_devices_and_30_day_totp_trusted_sessions: "Manage authorized devices and 30-day TOTP trusted sessions.",
txt_manage_device_sessions_and_30_day_totp_trusted_sessions: "Manage device sessions and 30-day TOTP trusted sessions.",
txt_master_password: "Master Password",
txt_master_password_changed_please_login_again: "Master password changed. Please login again.",
txt_master_password_is_required: "Master password is required",
@@ -301,7 +302,11 @@ const messages: Record<Locale, Record<string, string>> = {
txt_ignore: "Ignore",
txt_remove_device: "Remove device",
txt_remove_device_2: "Remove Device",
txt_remove_all_devices: "Remove all devices",
txt_remove_all_devices_and_clear_all_2fa_trust: "Remove all devices and clear all 2FA trust?",
txt_remove_all_devices_and_sign_out_all_sessions: "Remove all devices, clear all trust, and sign out every device?",
txt_remove_device_name_and_clear_its_2fa_trust: "Remove device \"{name}\" and clear its 2FA trust?",
txt_remove_device_and_sign_out_name: "Remove device \"{name}\", clear its trust, and sign it out?",
txt_reveal: "Reveal",
txt_revoke: "Revoke",
txt_revoke_30_day_totp_trust_for_name: "Revoke 30-day TOTP trust for \"{name}\"?",
@@ -384,6 +389,7 @@ const messages: Record<Locale, Record<string, string>> = {
txt_unlock_vault: "Unlock Vault",
txt_unignore: "Unignore",
txt_unlocked: "Unlocked",
txt_all_devices_removed: "All devices removed",
txt_update_item_failed: "Update item failed",
txt_update_send_failed: "Update send failed",
txt_use_recovery_code: "Use Recovery Code",
@@ -610,6 +616,7 @@ const zhCNOverrides: Record<string, string> = {
txt_copy_secret: '复制密钥',
txt_this_is_a_one_time_code_after_it_is_used_a_new_code_is_generated_automatically: '这是一次性恢复代码,使用后将自动生成新的恢复代码。',
txt_manage_authorized_devices_and_30_day_totp_trusted_sessions: '管理已授权设备和 30 天 TOTP 受信会话。',
txt_manage_device_sessions_and_30_day_totp_trusted_sessions: '管理设备会话和 30 天 TOTP 受信状态。',
txt_role: '角色',
txt_status: '状态',
txt_actions: '操作',
@@ -619,6 +626,10 @@ const zhCNOverrides: Record<string, string> = {
txt_revoke_30_day_totp_trust_from_all_devices: '确认撤销所有设备的 30 天 TOTP 信任吗?',
txt_revoke_30_day_totp_trust_for_name: '确认撤销“{name}”的 30 天 TOTP 信任吗?',
txt_remove_device_name_and_clear_its_2fa_trust: '确认移除设备“{name}”并清除其 2FA 信任吗?',
txt_remove_all_devices: '移除所有设备',
txt_remove_all_devices_and_clear_all_2fa_trust: '确认移除所有设备并清除全部 2FA 信任吗?',
txt_remove_all_devices_and_sign_out_all_sessions: '确认移除所有设备、清除全部信任,并让所有设备重新登录吗?',
txt_remove_device_and_sign_out_name: '确认移除设备“{name}”、清除其信任,并让它重新登录吗?',
txt_role_admin: '管理员',
txt_role_user: '用户',
txt_status_active: '正常',
@@ -766,6 +777,7 @@ const zhCNOverrides: Record<string, string> = {
txt_unlock_failed: '解锁失败',
txt_unlock_failed_master_password_is_incorrect: '解锁失败,主密码不正确。',
txt_unlocked: '已解锁',
txt_all_devices_removed: '已移除所有设备',
txt_update_item_failed: '更新项目失败',
txt_update_send_failed: '更新发送失败',
txt_use_your_one_time_recovery_code_to_disable_two_step_verification: '使用一次性恢复代码禁用两步验证。',