added 2FA

This commit is contained in:
2024-04-26 22:16:21 +03:00
parent 53cadc289a
commit f17a002ac6
38 changed files with 1036 additions and 414 deletions

View File

@@ -1,65 +0,0 @@
'use client'
//https://gist.github.com/mjbalcueva/b21f39a8787e558d4c536bf68e267398
import { forwardRef, useState } from 'react'
import { EyeIcon, EyeOffIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input, InputProps } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { FormControl } from '@/components/ui/form'
const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false)
const disabled = props.value === '' || props.value === undefined ||
props.disabled
return (<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
className={cn('hide-password-toggle pr-10', className)}
ref={ref}
{...props}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword((prev) => !prev)}
disabled={disabled}
>
{showPassword && !disabled ? (
<EyeIcon
className="h-4 w-4"
aria-hidden="true"
/>
) : (
<EyeOffIcon
className="h-4 w-4"
aria-hidden="true"
/>
)}
<span className="sr-only">
{showPassword ? 'Hide password' : 'Show password'}
</span>
</Button>
{/* hides browsers password toggles */}
<style>{`
.hide-password-toggle::-ms-reveal,
.hide-password-toggle::-ms-clear {
visibility: hidden;
pointer-events: none;
display: none;
}
`}</style>
</div>
)
},
)
PasswordInput.displayName = 'PasswordInput'
export { PasswordInput }

View File

@@ -1,10 +1,9 @@
'use client'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import { Header } from '@/components/auth/header'
import { Social } from '@/components/auth/social'
import { BackButton } from '@/components/auth/back-button'
import { Suspense } from 'react'
import { Loading } from '@/components/loading'
type Props = {
children: React.ReactNode
@@ -26,31 +25,29 @@ export const CardWrapper = ({
continueWithLabel,
}: Props) => {
return (
<Suspense fallback={<Loading/>}>
<Card
className="border-8 border-muted shadow-2xl max-w-[430px] w-full sm:min-w-[430px]">
<CardHeader>
<Header label={headerLabel} title={headerTitle}/>
</CardHeader>
<CardContent>
{children}
</CardContent>
{showSocial && <CardFooter className="flex-wrap">
<div className="relative flex-none w-[100%] mb-4" style={{ background: 'block' }}>
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<Card
className="shadow-2xl max-w-[430px] w-full sm:min-w-[430px]">
<CardHeader>
<Header label={headerLabel} title={headerTitle}/>
</CardHeader>
<CardContent>
{children}
</CardContent>
{showSocial && <CardFooter className="flex-wrap">
<div className="relative flex-none w-[100%] mb-4" style={{ background: 'block' }}>
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span
className="bg-background px-2 text-muted-foreground">{continueWithLabel}</span>
</div>
</div>
<Social/>
</CardFooter>}
<CardFooter>
<BackButton label={backButtonLabel} href={backButtonHref}/>
</CardFooter>
</Card>
</Suspense>
</div>
<Social/>
</CardFooter>}
<CardFooter>
<BackButton label={backButtonLabel} href={backButtonHref}/>
</CardFooter>
</Card>
)
}

View File

@@ -5,14 +5,7 @@ import { useState, useTransition } from 'react'
import { useForm } from 'react-hook-form'
import { useSearchParams } from 'next/navigation'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { useI18n } from '@/locales/client'
@@ -28,10 +21,9 @@ export const LoginForm = () => {
const t = useI18n()
const searchParams = useSearchParams()
const urlError = searchParams.get('error') === 'OAuthAccountNotLinked'
? t('auth.form.error.email_in_use')
: ''
const urlError = searchParams.get('error') === 'OAuthAccountNotLinked' ? t('auth.form.error.email_in_use') : ''
const [showTwoFactor, setShowTwoFactor] = useState<boolean>(false)
const [error, setError] = useState<string | undefined>('')
const [success, setSuccess] = useState<string | undefined>('')
const [isPending, startTransition] = useTransition()
@@ -48,10 +40,26 @@ export const LoginForm = () => {
startTransition(() => {
login(values).then((data) => {
// @ts-ignore
setError(t(data?.error))
// @ts-ignore
setSuccess(t(data?.success))
//@ts-ignore
if (data?.error) {
form.reset() //@ts-ignore
setError(t(data?.error))
}
//@ts-ignore
if (data?.success) {
form.reset() //@ts-ignore
setSuccess(t(data?.success))
}
//@ts-ignore
if (data?.twoFactor) { //@ts-ignore
setShowTwoFactor(data?.twoFactor)
}
}).catch((err) => {
setError('auth.common.something_went_wrong')
//TODO: do logging
console.log(err)
})
})
}
@@ -67,48 +75,62 @@ export const LoginForm = () => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-0"
className={showTwoFactor ? 'space-y-6' : 'space-y-2'}
>
<div className="space-y-4">
<FormField control={form.control} name="email"
render={({ field }) => (<FormItem>
<FormLabel>{t('form.label.email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder={t('form.placeholder.email')}
type="email"
autoComplete="email"
/>
</FormControl>
<FormMessage className="text-xs"/>
</FormItem>)}/>
{/*Password*/}
<FormField control={form.control} name="password"
render={({ field }) => (<FormItem>
<FormLabel>{t('form.label.password')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="******"
type="password"
autoComplete="current-password"
/>
</FormControl>
<Button variant="link" size="sm" asChild
className="mt-0 p-0 items-start font-light text-sky-900">
<Link href={AUTH_RESET_PASSWORD_URL}>{t('auth.form.login.reset_password_link_text')}</Link>
</Button>
<FormMessage className="text-xs"/>
</FormItem>)}/>
{showTwoFactor && (
<FormField control={form.control} name="code"
render={({ field }) => (<FormItem>
<FormLabel>{t('form.label.two_factor')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="¹₂³₄⁵₆"
/>
</FormControl>
<FormMessage className="text-xs"/>
</FormItem>)}/>
)}
{!showTwoFactor && (<>
<FormField control={form.control} name="email"
render={({ field }) => (<FormItem>
<FormLabel>{t('form.label.email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder={t('form.placeholder.email')}
type="email"
autoComplete="email"
/>
</FormControl>
<FormMessage className="text-xs"/>
</FormItem>)}/>
<FormField control={form.control} name="password"
render={({ field }) => (<FormItem>
<FormLabel>{t('form.label.password')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="******"
type="password"
autoComplete="current-password"
/>
</FormControl>
<Button variant="link" size="sm" asChild
className="mt-0 p-0 items-start font-light text-sky-900">
<Link href={AUTH_RESET_PASSWORD_URL}>{t('auth.form.login.reset_password_link_text')}</Link>
</Button>
<FormMessage className="text-xs"/>
</FormItem>)}/>
</>)}
</div>
<FormSuccess message={success}/>
<FormError message={error || urlError}/>
<Button type="submit" className="w-full" disabled={isPending}>
{t('form.label.login')}
{showTwoFactor ? t('form.button.two_factor') : t('form.button.login')}
</Button>
</form>
</Form>

View File

@@ -58,7 +58,7 @@ export const NewPasswordForm = ({ token }: { token: string }) => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
className="space-y-6"
>
<div className="space-y-4">
<FormField control={form.control} name="password"

View File

@@ -58,7 +58,7 @@ export const ResetForm = () => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
className="space-y-6"
>
<div className="space-y-4">
<FormField control={form.control} name="email"