AdminDisableTwoFactor.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { useState } from 'react';
  2. import { useMutation } from '@tanstack/react-query';
  3. import { ShieldOff } from 'lucide-react';
  4. import { useTranslation } from 'react-i18next';
  5. import { api, ApiError } from '../api/client';
  6. import { Button } from './Button';
  7. import { useToast } from '../contexts/ToastContext';
  8. import { useAuth } from '../contexts/AuthContext';
  9. interface AdminDisableTwoFactorProps {
  10. userId: number;
  11. username: string;
  12. }
  13. /**
  14. * Edit user: remove a user's 2FA (TOTP, email codes and backup codes), for a
  15. * user locked out of their authenticator. Admin-only, like the route. Not shown
  16. * on your own account, which has its own 2FA settings. The route asks for the
  17. * admin's own password unless the admin has none (LDAP and OIDC accounts,
  18. * unless they were given one).
  19. */
  20. export function AdminDisableTwoFactor({ userId, username }: AdminDisableTwoFactorProps) {
  21. const { t } = useTranslation();
  22. const { showToast } = useToast();
  23. const { authEnabled, isAdmin, user } = useAuth();
  24. const [confirming, setConfirming] = useState(false);
  25. const [password, setPassword] = useState('');
  26. // Set when the route asks for a password the sign-in type did not suggest:
  27. // an LDAP or SSO account that was given a local password in Edit user.
  28. const [passwordAsked, setPasswordAsked] = useState(false);
  29. const needsPassword = passwordAsked || user?.auth_source === 'local';
  30. const disableMutation = useMutation({
  31. mutationFn: () => api.admin2FADisable(userId, needsPassword ? password : null),
  32. onSuccess: (response) => {
  33. setConfirming(false);
  34. setPassword('');
  35. if (response.had_2fa) {
  36. showToast(t('users.form.twoFaRemoved', { username }), 'success');
  37. } else {
  38. showToast(t('users.form.twoFaNoneToRemove', { username }), 'info');
  39. }
  40. },
  41. onError: (error: Error) => {
  42. if (!needsPassword && error instanceof ApiError && error.status === 401) {
  43. setPasswordAsked(true);
  44. showToast(t('users.form.twoFaPasswordNeeded'), 'info');
  45. return;
  46. }
  47. showToast(error.message, 'error');
  48. },
  49. });
  50. if (!authEnabled || !isAdmin || user?.id === userId) return null;
  51. return (
  52. <div className="bg-bambu-dark-secondary/50 border border-bambu-dark-tertiary rounded-lg p-3 space-y-3">
  53. <div>
  54. <p className="text-sm font-medium text-white">{t('users.form.twoFaTitle')}</p>
  55. <p className="text-sm text-bambu-gray">{t('users.form.twoFaHint')}</p>
  56. </div>
  57. {confirming ? (
  58. <div className="space-y-3">
  59. {needsPassword && (
  60. <div>
  61. <label htmlFor="admin-disable-2fa-password" className="block text-sm font-medium text-white mb-1">
  62. {t('users.form.twoFaYourPassword')}
  63. </label>
  64. <input
  65. id="admin-disable-2fa-password"
  66. type="password"
  67. value={password}
  68. onChange={(e) => setPassword(e.target.value)}
  69. autoComplete="current-password"
  70. className="w-full px-4 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green"
  71. />
  72. </div>
  73. )}
  74. <div className="flex gap-3">
  75. <Button
  76. variant="secondary"
  77. size="sm"
  78. className="flex-1"
  79. onClick={() => {
  80. setConfirming(false);
  81. setPassword('');
  82. }}
  83. >
  84. {t('common.cancel')}
  85. </Button>
  86. <Button
  87. variant="danger"
  88. size="sm"
  89. className="flex-1"
  90. disabled={disableMutation.isPending || (needsPassword && !password)}
  91. onClick={() => disableMutation.mutate()}
  92. >
  93. {t('common.confirm')}
  94. </Button>
  95. </div>
  96. </div>
  97. ) : (
  98. <Button variant="secondary" size="sm" className="w-full" onClick={() => setConfirming(true)}>
  99. <ShieldOff className="w-4 h-4" />
  100. {t('users.form.twoFaDisable')}
  101. </Button>
  102. )}
  103. </div>
  104. );
  105. }