255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
import { Form, Head } from '@inertiajs/react';
|
|
import { ShieldCheck } from 'lucide-react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController';
|
|
import Heading from '@/components/heading';
|
|
import InputError from '@/components/input-error';
|
|
import PasswordInput from '@/components/password-input';
|
|
import TwoFactorRecoveryCodes from '@/components/two-factor-recovery-codes';
|
|
import TwoFactorSetupModal from '@/components/two-factor-setup-modal';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth';
|
|
import { useTranslation } from '@/lib/translations';
|
|
import { edit } from '@/routes/security';
|
|
import { disable, enable } from '@/routes/two-factor';
|
|
|
|
type Props = {
|
|
canManageTwoFactor?: boolean;
|
|
requiresConfirmation?: boolean;
|
|
twoFactorEnabled?: boolean;
|
|
passwordRules: string;
|
|
};
|
|
|
|
export default function Security({
|
|
canManageTwoFactor = false,
|
|
requiresConfirmation = false,
|
|
twoFactorEnabled = false,
|
|
passwordRules,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const passwordInput = useRef<HTMLInputElement>(null);
|
|
const currentPasswordInput = useRef<HTMLInputElement>(null);
|
|
|
|
const {
|
|
qrCodeSvg,
|
|
hasSetupData,
|
|
manualSetupKey,
|
|
clearSetupData,
|
|
clearTwoFactorAuthData,
|
|
fetchSetupData,
|
|
recoveryCodesList,
|
|
fetchRecoveryCodes,
|
|
errors,
|
|
} = useTwoFactorAuth();
|
|
const [showSetupModal, setShowSetupModal] = useState<boolean>(false);
|
|
const prevTwoFactorEnabled = useRef(twoFactorEnabled);
|
|
|
|
useEffect(() => {
|
|
if (prevTwoFactorEnabled.current && !twoFactorEnabled) {
|
|
clearTwoFactorAuthData();
|
|
}
|
|
|
|
prevTwoFactorEnabled.current = twoFactorEnabled;
|
|
}, [twoFactorEnabled, clearTwoFactorAuthData]);
|
|
|
|
return (
|
|
<>
|
|
<Head title={t('Security settings')} />
|
|
|
|
<h1 className="sr-only">{t('Security settings')}</h1>
|
|
|
|
<div className="space-y-6">
|
|
<Heading
|
|
variant="small"
|
|
title="Update password"
|
|
description="Ensure your account is using a long, random password to stay secure"
|
|
/>
|
|
|
|
<Form
|
|
{...SecurityController.update.form()}
|
|
options={{
|
|
preserveScroll: true,
|
|
}}
|
|
resetOnError={[
|
|
'password',
|
|
'password_confirmation',
|
|
'current_password',
|
|
]}
|
|
resetOnSuccess
|
|
onError={(errors) => {
|
|
if (errors.password) {
|
|
passwordInput.current?.focus();
|
|
}
|
|
|
|
if (errors.current_password) {
|
|
currentPasswordInput.current?.focus();
|
|
}
|
|
}}
|
|
className="space-y-6"
|
|
>
|
|
{({ errors, processing }) => (
|
|
<>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="current_password">
|
|
Current password
|
|
</Label>
|
|
|
|
<PasswordInput
|
|
id="current_password"
|
|
ref={currentPasswordInput}
|
|
name="current_password"
|
|
className="mt-1 block w-full"
|
|
autoComplete="current-password"
|
|
placeholder={t('Current password')}
|
|
/>
|
|
|
|
<InputError message={errors.current_password} />
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="password">New password</Label>
|
|
|
|
<PasswordInput
|
|
id="password"
|
|
ref={passwordInput}
|
|
name="password"
|
|
className="mt-1 block w-full"
|
|
autoComplete="new-password"
|
|
placeholder={t('New password')}
|
|
passwordrules={passwordRules}
|
|
/>
|
|
|
|
<InputError message={errors.password} />
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="password_confirmation">
|
|
Confirm password
|
|
</Label>
|
|
|
|
<PasswordInput
|
|
id="password_confirmation"
|
|
name="password_confirmation"
|
|
className="mt-1 block w-full"
|
|
autoComplete="new-password"
|
|
placeholder={t('Confirm password')}
|
|
passwordrules={passwordRules}
|
|
/>
|
|
|
|
<InputError
|
|
message={errors.password_confirmation}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<Button
|
|
disabled={processing}
|
|
data-test="update-password-button"
|
|
>
|
|
Save password
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
|
|
{canManageTwoFactor && (
|
|
<div className="space-y-6">
|
|
<Heading
|
|
variant="small"
|
|
title="Two-factor authentication"
|
|
description="Manage your two-factor authentication settings"
|
|
/>
|
|
{twoFactorEnabled ? (
|
|
<div className="flex flex-col items-start justify-start space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(
|
|
'You will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.',
|
|
)}
|
|
</p>
|
|
|
|
<div className="relative inline">
|
|
<Form {...disable.form()}>
|
|
{({ processing }) => (
|
|
<Button
|
|
variant="destructive"
|
|
type="submit"
|
|
disabled={processing}
|
|
>
|
|
Disable 2FA
|
|
</Button>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
|
|
<TwoFactorRecoveryCodes
|
|
recoveryCodesList={recoveryCodesList}
|
|
fetchRecoveryCodes={fetchRecoveryCodes}
|
|
errors={errors}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-start justify-start space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(
|
|
'When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.',
|
|
)}
|
|
</p>
|
|
|
|
<div>
|
|
{hasSetupData ? (
|
|
<Button
|
|
onClick={() => setShowSetupModal(true)}
|
|
>
|
|
<ShieldCheck />
|
|
Continue setup
|
|
</Button>
|
|
) : (
|
|
<Form
|
|
{...enable.form()}
|
|
onSuccess={() =>
|
|
setShowSetupModal(true)
|
|
}
|
|
>
|
|
{({ processing }) => (
|
|
<Button
|
|
type="submit"
|
|
disabled={processing}
|
|
>
|
|
Enable 2FA
|
|
</Button>
|
|
)}
|
|
</Form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<TwoFactorSetupModal
|
|
isOpen={showSetupModal}
|
|
onClose={() => setShowSetupModal(false)}
|
|
requiresConfirmation={requiresConfirmation}
|
|
twoFactorEnabled={twoFactorEnabled}
|
|
qrCodeSvg={qrCodeSvg}
|
|
manualSetupKey={manualSetupKey}
|
|
clearSetupData={clearSetupData}
|
|
fetchSetupData={fetchSetupData}
|
|
errors={errors}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
Security.layout = {
|
|
breadcrumbs: [
|
|
{
|
|
title: 'Security settings',
|
|
href: edit(),
|
|
},
|
|
],
|
|
};
|