first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import {useState} from 'react';
import {User} from '@common/auth/user';
import {TwoFactorDisabledStep} from '@common/auth/ui/two-factor/stepper/two-factor-disabled-step';
import {TwoFactorConfirmationStep} from '@common/auth/ui/two-factor/stepper/two-factor-confirmation-step';
import {TwoFactorEnabledStep} from '@common/auth/ui/two-factor/stepper/two-factor-enabled-step';
enum Status {
Disabled,
WaitingForConfirmation,
Enabled,
}
interface Props {
user: User;
}
export function TwoFactorStepper({user}: Props) {
const [status, setStatus] = useState<Status>(getStatus(user));
switch (status) {
case Status.Disabled:
return (
<TwoFactorDisabledStep
onEnabled={() => setStatus(Status.WaitingForConfirmation)}
/>
);
case Status.WaitingForConfirmation:
return (
<TwoFactorConfirmationStep
onCancel={() => {
setStatus(Status.Disabled);
}}
onConfirmed={() => {
setStatus(Status.Enabled);
}}
/>
);
case Status.Enabled:
return (
<TwoFactorEnabledStep
user={user}
onDisabled={() => setStatus(Status.Disabled)}
/>
);
}
}
function getStatus(user: User): Status {
if (user.two_factor_confirmed_at) {
return Status.Enabled;
} else if (user.two_factor_recovery_codes) {
return Status.WaitingForConfirmation;
}
return Status.Disabled;
}

View File

@@ -0,0 +1,125 @@
import {AnimatePresence, m} from 'framer-motion';
import {opacityAnimation} from '@common/ui/animation/opacity-animation';
import {ReactNode} from 'react';
import {useTwoFactorQrCode} from '@common/auth/ui/two-factor/requests/use-two-factor-qr-code';
import {useForm} from 'react-hook-form';
import {
ConfirmTwoFactorPayload,
useConfirmTwoFactor,
} from '@common/auth/ui/two-factor/requests/use-confirm-two-factor';
import {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';
import {Trans} from '@common/i18n/trans';
import {Skeleton} from '@common/ui/skeleton/skeleton';
import {Form} from '@common/ui/forms/form';
import {queryClient} from '@common/http/query-client';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import {useDisableTwoFactor} from '@common/auth/ui/two-factor/requests/use-disable-two-factor';
import {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';
import {Button} from '@common/ui/buttons/button';
interface Props {
onCancel: () => void;
onConfirmed: () => void;
}
export function TwoFactorConfirmationStep(props: Props) {
const {data} = useTwoFactorQrCode();
return (
<TwoFactorStepperLayout
title={<Trans message="Finish enabling two factor authentication." />}
description={
<Trans message="To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application or enter the setup key and provide the generated OTP code." />
}
>
<AnimatePresence initial={false}>
{!data ? (
<QrCodeLayout
animationKey="svg-skeleton"
svg={<Skeleton variant="rect" size="w-full h-full" />}
secret={<Skeleton variant="text" className="max-w-224" />}
/>
) : (
<QrCodeLayout
animationKey="real-svg"
svg={<div dangerouslySetInnerHTML={{__html: data.svg}} />}
secret={
<Trans message="Setup key: :key" values={{key: data.secret}} />
}
/>
)}
</AnimatePresence>
<CodeForm {...props} />
</TwoFactorStepperLayout>
);
}
function CodeForm({onCancel, onConfirmed}: Props) {
const form = useForm<ConfirmTwoFactorPayload>();
const confirmTwoFactor = useConfirmTwoFactor(form);
const disableTwoFactor = useDisableTwoFactor();
const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =
usePasswordConfirmedAction();
const isLoading =
confirmTwoFactor.isPending ||
disableTwoFactor.isPending ||
confirmPasswordIsLoading;
return (
<Form
form={form}
onSubmit={values =>
withConfirmedPassword(() => {
confirmTwoFactor.mutate(values, {
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ['users']});
onConfirmed();
},
});
})
}
>
<FormTextField
required
name="code"
label={<Trans message="Code" />}
autoFocus
/>
<div className="flex items-center gap-12 mt-24">
<Button
type="button"
variant="outline"
disabled={isLoading}
onClick={() => {
withConfirmedPassword(() => {
disableTwoFactor.mutate(undefined, {onSuccess: onCancel});
});
}}
>
<Trans message="Cancel" />
</Button>
<Button
type="submit"
variant="flat"
color="primary"
disabled={isLoading}
>
<Trans message="Confirm" />
</Button>
</div>
</Form>
);
}
interface QrCodeLayoutProps {
animationKey: string;
svg: ReactNode;
secret: ReactNode;
}
function QrCodeLayout({animationKey, svg, secret}: QrCodeLayoutProps) {
return (
<m.div key={animationKey} {...opacityAnimation}>
<div className="w-192 h-192 mb-16">{svg}</div>
<div className="text-sm font-medium mb-16">{secret}</div>
</m.div>
);
}

View File

@@ -0,0 +1,39 @@
import {useEnableTwoFactor} from '@common/auth/ui/two-factor/requests/use-enable-two-factor';
import {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';
import {Trans} from '@common/i18n/trans';
import {Button} from '@common/ui/buttons/button';
import {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';
interface Props {
onEnabled: () => void;
}
export function TwoFactorDisabledStep({onEnabled}: Props) {
const enableTwoFactor = useEnableTwoFactor();
const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =
usePasswordConfirmedAction();
const isLoading = enableTwoFactor.isPending || confirmPasswordIsLoading;
return (
<TwoFactorStepperLayout
title={
<Trans message="You have not enabled two factor authentication." />
}
actions={
<Button
variant="flat"
color="primary"
disabled={isLoading}
onClick={() => {
withConfirmedPassword(() => {
enableTwoFactor.mutate(undefined, {
onSuccess: onEnabled,
});
});
}}
>
<Trans message="Enable" />
</Button>
}
/>
);
}

View File

@@ -0,0 +1,84 @@
import {User} from '@common/auth/user';
import {useDisableTwoFactor} from '@common/auth/ui/two-factor/requests/use-disable-two-factor';
import {useRegenerateTwoFactorCodes} from '@common/auth/ui/two-factor/requests/use-regenerate-two-factor-codes';
import {Fragment} from 'react';
import {queryClient} from '@common/http/query-client';
import {Trans} from '@common/i18n/trans';
import {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';
import {Button} from '@common/ui/buttons/button';
interface Props {
user: User;
onDisabled: () => void;
}
export function TwoFactorEnabledStep({user, onDisabled}: Props) {
const disableTwoFactor = useDisableTwoFactor();
const regenerateCodes = useRegenerateTwoFactorCodes();
const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =
usePasswordConfirmedAction();
const isLoading =
disableTwoFactor.isPending ||
regenerateCodes.isPending ||
confirmPasswordIsLoading;
const actions = (
<Fragment>
<Button
type="button"
onClick={() =>
withConfirmedPassword(() => {
regenerateCodes.mutate(undefined, {
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ['users']});
},
});
})
}
variant="outline"
disabled={isLoading}
className="mr-12"
>
<Trans message="Regenerate recovery codes" />
</Button>
<Button
type="submit"
variant="flat"
color="danger"
disabled={isLoading}
onClick={() => {
withConfirmedPassword(() => {
disableTwoFactor.mutate(undefined, {
onSuccess: () => {
toast(message('Two factor authentication has been disabled.'));
onDisabled();
},
});
});
}}
>
<Trans message="Disable" />
</Button>
</Fragment>
);
return (
<TwoFactorStepperLayout
title={<Trans message="You have enabled two factor authentication." />}
description={
<Trans message="Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost." />
}
actions={actions}
>
<div className="bg-alt p-14 font-mono text-sm mb-16 rounded">
{user.two_factor_recovery_codes?.map(code => (
<div className="mb-4" key={code}>
{code}
</div>
))}
</div>
</TwoFactorStepperLayout>
);
}

View File

@@ -0,0 +1,32 @@
import {Fragment, ReactNode} from 'react';
import {Trans} from '@common/i18n/trans';
interface Props {
title: ReactNode;
subtitle?: ReactNode;
description?: ReactNode;
actions?: ReactNode;
children?: ReactNode;
}
export function TwoFactorStepperLayout({
title,
subtitle,
description,
actions,
children,
}: Props) {
if (!subtitle) {
subtitle = (
<Trans message="When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application." />
);
}
return (
<Fragment>
<div className="text-base font-medium mb-16">{title}</div>
<div className="text-sm mb-24">{subtitle}</div>
<p className="text-sm font-medium my-16">{description}</p>
{children}
<div className="flex items-center gap-12">{actions}</div>
</Fragment>
);
}