Skip to content

Commit

Permalink
Merge pull request #23 from Design-System-Project/feature/tfr2-120-ot…
Browse files Browse the repository at this point in the history
…p-page

replace login with otp auth page
  • Loading branch information
tomasfrancisco authored Aug 30, 2024
2 parents a250ffe + 7eb9d44 commit 541e884
Show file tree
Hide file tree
Showing 29 changed files with 600 additions and 202 deletions.
4 changes: 3 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"human-id": "^4.1.1",
"memoize": "^10.0.0",
"next": "catalog:",
"next-safe-action": "^7.8.1",
"postgres": "^3.4.4",
"posthog-js": "^1.160.0",
"rambda": "^9.2.1",
Expand All @@ -51,7 +52,8 @@
"style-dictionary": "catalog:",
"superjson": "^2.2.1",
"tailwind-merge": "^2.4.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-form-data": "^2.0.2"
},
"devDependencies": {
"@ds-project/eslint": "workspace:*",
Expand Down
33 changes: 33 additions & 0 deletions apps/dashboard/src/app/auth/_actions/auth.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use server';

import { z } from 'zod';
import { config } from '@/config';
import { unprotectedAction } from '@/lib/safe-action';

export const authAction = unprotectedAction
.metadata({ actionName: 'authAction' })
.schema(
z.object({
email: z.string().email(),
})
)
.action(async ({ ctx, parsedInput: { email } }) => {
const { error } = await ctx.authClient.auth.signInWithOtp({
email: email,
options: {
shouldCreateUser: true,
emailRedirectTo: `${config.pageUrl}/auth/callback`,
},
});

if (!error) {
return {
ok: true,
};
}

return {
error: error.message,
ok: false,
};
});
36 changes: 36 additions & 0 deletions apps/dashboard/src/app/auth/_actions/verify-otp.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use server';

import { z } from 'zod';
import { unprotectedAction } from '@/lib/safe-action';
import { zfd } from 'zod-form-data';

export const verifyOtpAction = unprotectedAction
.metadata({ actionName: 'verifyOtpAction' })
.schema(
z.object({
email: zfd.text(z.string().email()),
token: zfd.text(
z.string().min(6, {
message: 'Your one-time password must be 6 characters.',
})
),
})
)
.action(async ({ ctx, parsedInput: { email, token } }) => {
const { error } = await ctx.authClient.auth.verifyOtp({
email,
token,
type: 'email',
});

if (!error) {
return {
ok: true,
};
}

return {
error: error.message,
ok: false,
};
});
1 change: 1 addition & 0 deletions apps/dashboard/src/app/auth/_assets/auth-bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions apps/dashboard/src/app/auth/_components/auth-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
DSLogo,
} from '@ds-project/components';
import Image from 'next/image';
import authBackground from '../_assets/auth-bg.svg';
import { AuthForm } from './auth-form';

export function AuthCard() {
return (
<Card className="w-full max-w-sm">
<CardHeader className="flex flex-col items-center relative">
<Image
src={authBackground}
alt="auth background"
className="absolute top-0"
/>
<DSLogo width={64} height={64} className="z-10" />
<CardTitle className="text-2xl" weight="normal">
<h1>Sign in to DS</h1>
</CardTitle>
<CardDescription weight="light">
<p>We'll email you a code for a password-free sign in.</p>
</CardDescription>
</CardHeader>

<CardContent className="grid gap-4">
<AuthForm />
</CardContent>
</Card>
);
}
161 changes: 161 additions & 0 deletions apps/dashboard/src/app/auth/_components/auth-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client';

import {
Button,
cn,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Icons,
Input,
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@ds-project/components';
import { authAction } from '../_actions/auth.action';
import { ErrorMessage } from './error-message';
import { verifyOtpAction } from '../_actions/verify-otp.action';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

const FormSchema = z.object({
email: z.string().email({
message: 'Please enter a valid email address.',
}),
otpToken: z
.string()
.min(6, {
message: 'Your one-time password must be 6 characters.',
})
.optional(),
});

export const AuthForm = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [stage, setStage] = useState<'signin' | 'verify'>('signin');
const [error, setError] = useState<string>();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: '',
otpToken: undefined,
},
});

async function onSubmit(data: z.infer<typeof FormSchema>) {
setLoading(true);
if (stage === 'verify' && data.otpToken) {
const result = await verifyOtpAction({
email: data.email,
token: data.otpToken,
});

if (result?.data?.ok) {
router.replace('/app');
}

if (result?.data?.error) {
setError(result.data.error);
}
} else {
const result = await authAction({
email: data.email,
});

if (result?.data?.ok) {
setStage('verify');
}

if (result?.data?.error) {
setError(result.data.error);
}
}
setLoading(false);
}

return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem hidden={stage === 'verify'}>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
autoComplete="email"
placeholder="[email protected]"
{...field}
/>
</FormControl>
</FormItem>
)}
/>

<FormField
control={form.control}
name="otpToken"
render={({ field }) => (
<FormItem
className={cn('flex flex-col items-center', {
hidden: stage === 'signin',
})}
>
<FormLabel>Code</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Please enter the code sent to your email.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

{/* <Message visible={signInFormState.data?.ok} /> */}
<ErrorMessage
error={
form.formState.errors.email?.message ??
form.formState.errors.otpToken?.message ??
error
}
/>

<Button className="w-full" aria-disabled={loading} type="submit">
<>
{loading ? (
<Icons.SymbolIcon className="mr-2 size-4 animate-spin" />
) : null}
{loading
? 'Sending...'
: stage === 'signin'
? 'Sign in with email'
: 'Continue'}
</>
</Button>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,18 @@ import {
} from '@ds-project/components';
import { AnimatePresence, motion } from 'framer-motion';

export const Message = ({
visible = false,
email,
}: {
visible?: boolean;
email?: string;
}) => (
export const ErrorMessage = ({ error }: { error?: string }) => (
<AnimatePresence>
{visible ? (
<Alert asChild variant="info">
{error ? (
<Alert asChild variant="destructive">
<motion.div
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
>
<Icons.EnvelopeOpenIcon className="size-4" />
<AlertTitle>Check your inbox</AlertTitle>
<AlertDescription>
An email has been sent to {email} with a magic link.
</AlertDescription>
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</motion.div>
</Alert>
) : null}
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/src/app/auth/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<div className={'min-h-screen flex flex-col justify-center items-center'}>
<div
className={
'min-h-screen flex flex-col justify-center items-center w-full'
}
>
{children}
</div>
);
Expand Down
1 change: 0 additions & 1 deletion apps/dashboard/src/app/auth/login/_actions/index.ts

This file was deleted.

41 changes: 0 additions & 41 deletions apps/dashboard/src/app/auth/login/_actions/login-user.action.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/dashboard/src/app/auth/login/_components/index.ts

This file was deleted.

Loading

0 comments on commit 541e884

Please sign in to comment.