fix: add layout and minor updates

This commit is contained in:
Mythie
2023-09-19 13:34:54 +00:00
parent 0060b9da8c
commit ca325cc90b
20 changed files with 238 additions and 298 deletions

View File

@ -1,20 +1,32 @@
{ {
"name": "Documenso", "name": "Documenso",
"image": "mcr.microsoft.com/devcontainers/base:bullseye", "image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": { "features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": { "ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest", "version": "latest",
"enableNonRootDocker": "true", "enableNonRootDocker": "true",
"moby": "true" "moby": "true"
}, },
"ghcr.io/devcontainers/features/node:1": {} "ghcr.io/devcontainers/features/node:1": {}
}, },
"onCreateCommand": "./.devcontainer/on-create.sh", "onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [ "forwardPorts": [3000, 54320, 9000, 2500, 1100],
3000, "customizations": {
54320, "vscode": {
9000, "extensions": [
2500, "GitHub.vscode-pull-request-github",
1100 "GitHub.copilot-labs",
] "GitHub.copilot-chat",
"GitHub.copilot",
"aaron-bond.better-comments",
"mikestead.dotenv",
"VisualStudioExptTeam.vscodeintellicode",
"Prisma.prisma",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"unifiedjs.vscode-mdx",
"esbenp.prettier-vscode"
]
}
}
} }

View File

@ -1,34 +1,20 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png'; import { Button } from '@documenso/ui/primitives/button';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div>
<div className="relative flex w-1/5 items-center gap-x-24"> <h1 className="text-4xl font-semibold">Email sent!</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="w-full text-center"> <p className="text-muted-foreground mb-4 mt-2 text-sm">
<h1 className="text-4xl font-semibold">Reset Pasword</h1> A password reset email has been sent, if you have an account you should see it in your inbox
shortly.
</p>
<p className="text-muted-foreground/60 mb-4 mt-2 text-sm"> <Button asChild>
Please check your email for reset instructions <Link href="/signin">Return to sign in</Link>
</p> </Button>
</div>
<p className="text-muted-foreground mt-6 text-center text-sm">
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Sign in
</Link>
</p>
</div>
</div>
</main>
); );
} }

View File

@ -1,38 +1,25 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png';
import { ForgotPasswordForm } from '~/components/forms/forgot-password'; import { ForgotPasswordForm } from '~/components/forms/forgot-password';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div>
<div className="relative flex items-center gap-x-24 md:w-[500px]"> <h1 className="text-4xl font-semibold">Forgotten your password?</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="w-full"> <p className="text-muted-foreground mt-2 text-sm">
<h1 className="text-4xl font-semibold">Forgot Password?</h1> No worries, it happens! Enter your email and we'll email you a special link to reset your
password.
</p>
<p className="text-muted-foreground/60 mt-2 text-sm"> <ForgotPasswordForm className="mt-4" />
No worries, we'll send you reset instructions.
</p>
<ForgotPasswordForm className="mt-4" /> <p className="text-muted-foreground mt-6 text-center text-sm">
Remembered your password?{' '}
<p className="text-muted-foreground mt-6 text-center text-sm"> <Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Don't have an account?{' '} Sign In
<Link href="/signup" className="text-primary duration-200 hover:opacity-70"> </Link>
Sign up </p>
</Link> </div>
</p>
</div>
</div>
</main>
); );
} }

View File

@ -0,0 +1,27 @@
import React from 'react';
import Image from 'next/image';
import backgroundPattern from '~/assets/background-pattern.png';
type UnauthenticatedLayoutProps = {
children: React.ReactNode;
};
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div className="relative flex w-full max-w-md items-center gap-x-24">
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="w-full">{children}</div>
</div>
</main>
);
}

View File

@ -1,36 +1,37 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { redirect } from 'next/navigation';
import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity';
import backgroundPattern from '~/assets/background-pattern.png';
import { ResetPasswordForm } from '~/components/forms/reset-password'; import { ResetPasswordForm } from '~/components/forms/reset-password';
export default function ResetPasswordPage({ params }: { params: { token: string } }) { type ResetPasswordPageProps = {
params: {
token: string;
};
};
export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) {
const isValid = await getResetTokenValidity({ token });
if (!isValid) {
redirect('/reset-password');
}
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div className="w-full">
<div className="relative flex items-center gap-x-24 md:w-[500px]"> <h1 className="text-4xl font-semibold">Reset Password</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="w-full"> <p className="text-muted-foreground mt-2 text-sm">Please choose your new password </p>
<h1 className="text-4xl font-semibold">Reset Password</h1>
<p className="text-muted-foreground/60 mt-2 text-sm">Please choose your new password </p> <ResetPasswordForm token={token} className="mt-4" />
<ResetPasswordForm token={params.token} className="mt-4" /> <p className="text-muted-foreground mt-6 text-center text-sm">
Don't have an account?{' '}
<p className="text-muted-foreground mt-6 text-center text-sm"> <Link href="/signup" className="text-primary duration-200 hover:opacity-70">
Don't have an account?{' '} Sign up
<Link href="/signup" className="text-primary duration-200 hover:opacity-70"> </Link>
Sign up </p>
</Link> </div>
</p>
</div>
</div>
</main>
); );
} }

View File

@ -1,34 +1,20 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png'; import { Button } from '@documenso/ui/primitives/button';
export default function ResetPasswordPage() { export default function ResetPasswordPage() {
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div>
<div className="relative flex items-center md:w-[500px]"> <h1 className="text-4xl font-semibold">Unable to reset password</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="w-full text-center"> <p className="text-muted-foreground mt-2 text-sm">
<h1 className="text-4xl font-semibold">Reset Password</h1> The token you have used to reset your password is either expired or it never existed. If you
have still forgotten your password, please request a new reset link.
</p>
<p className="text-muted-foreground/60 mt-2 text-sm"> <Button className="mt-4" asChild>
The token you provided is invalid. Please try again. <Link href="/signin">Return to sign in</Link>
</p> </Button>
</div>
<p className="text-muted-foreground mt-6 text-center text-sm">
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
Sign in
</Link>
</p>
</div>
</div>
</main>
); );
} }

View File

@ -1,43 +1,33 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png';
import connections from '~/assets/card-sharing-figure.png';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
export default function SignInPage() { export default function SignInPage() {
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div>
<div className="relative flex max-w-4xl items-center gap-x-24"> <h1 className="text-4xl font-semibold">Sign in to your account</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="max-w-md"> <p className="text-muted-foreground/60 mt-2 text-sm">
<h1 className="text-4xl font-semibold">Sign in to your account</h1> Welcome back, we are lucky to have you.
</p>
<p className="text-muted-foreground/60 mt-2 text-sm"> <SignInForm className="mt-4" />
Welcome back, we are lucky to have you.
</p>
<SignInForm className="mt-4" /> <p className="text-muted-foreground mt-6 text-center text-sm">
Don't have an account?{' '}
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
Sign up
</Link>
</p>
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="mt-2.5 text-center">
Don't have an account?{' '} <Link
<Link href="/signup" className="text-primary duration-200 hover:opacity-70"> href="/forgot-password"
Sign up className="text-muted-foreground text-sm duration-200 hover:opacity-70"
</Link> >
</p> Forgotten your password?
</div> </Link>
</p>
<div className="hidden flex-1 lg:block"> </div>
<Image src={connections} alt="documenso connections" />
</div>
</div>
</main>
); );
} }

View File

@ -1,44 +1,25 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png';
import connections from '~/assets/connections.png';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
export default function SignUpPage() { export default function SignUpPage() {
return ( return (
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24"> <div>
<div className="relative flex max-w-4xl items-center gap-x-24"> <h1 className="text-4xl font-semibold">Create a new account</h1>
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
<Image
src={backgroundPattern}
alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia"
/>
</div>
<div className="max-w-md"> <p className="text-muted-foreground/60 mt-2 text-sm">
<h1 className="text-4xl font-semibold">Create a shiny, new Documenso Account </h1> Create your account and start using state-of-the-art document signing. Open and beautiful
signing is within your grasp.
</p>
<p className="text-muted-foreground/60 mt-2 text-sm"> <SignUpForm className="mt-4" />
Create your account and start using state-of-the-art document signing. Open and
beautiful signing is within your grasp.
</p>
<SignUpForm className="mt-4" /> <p className="text-muted-foreground mt-6 text-center text-sm">
Already have an account?{' '}
<p className="text-muted-foreground mt-6 text-center text-sm"> <Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Already have an account?{' '} Sign in instead
<Link href="/signin" className="text-primary duration-200 hover:opacity-70"> </Link>
Sign in instead </p>
</Link> </div>
</p>
</div>
<div className="hidden flex-1 lg:block">
<Image src={connections} alt="documenso connections" />
</div>
</div>
</main>
); );
} }

View File

@ -3,14 +3,13 @@
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -44,33 +43,18 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation(); const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => { const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
try { await forgotPassword({ email }).catch(() => null);
await forgotPassword({ email });
toast({ toast({
title: 'Reset email sent', title: 'Reset email sent',
description: 'Your password reset mail has been sent successfully.', description:
duration: 5000, 'A password reset email has been sent, if you have an account you should see it in your inbox shortly.',
}); duration: 5000,
});
reset(); reset();
router.push('/check-email');
} catch (err) { router.push('/check-email');
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: 'An error occurred',
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: 'An unknown error occurred',
variant: 'destructive',
description:
'We encountered an unknown error while attempting to send your email. Please try again later.',
});
}
}
}; };
return ( return (
@ -79,17 +63,16 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div> <div>
<Label htmlFor="email" className="text-slate-500"> <Label htmlFor="email" className="text-muted-foreground">
Email Email
</Label> </Label>
<Input id="email" type="email" className="bg-background mt-2" {...register('email')} /> <Input id="email" type="email" className="bg-background mt-2" {...register('email')} />
{errors.email && <span className="mt-1 text-xs text-red-500">{errors.email.message}</span>} <FormErrorMessage className="mt-1.5" error={errors.email} />
</div> </div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90"> <Button size="lg" loading={isSubmitting}>
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
Reset Password Reset Password
</Button> </Button>
</form> </form>

View File

@ -88,7 +88,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div> <div>
<Label htmlFor="password" className="text-slate-500"> <Label htmlFor="password" className="text-muted-foreground">
Password Password
</Label> </Label>
@ -106,7 +106,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
</div> </div>
<div> <div>
<Label htmlFor="repeated-password" className="text-slate-500"> <Label htmlFor="repeated-password" className="text-muted-foreground">
Repeat Password Repeat Password
</Label> </Label>

View File

@ -89,7 +89,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div> <div>
<Label htmlFor="full-name" className="text-slate-500"> <Label htmlFor="full-name" className="text-muted-foreground">
Full Name Full Name
</Label> </Label>
@ -99,7 +99,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</div> </div>
<div> <div>
<Label htmlFor="email" className="text-slate-500"> <Label htmlFor="email" className="text-muted-foreground">
Email Email
</Label> </Label>
@ -107,7 +107,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</div> </div>
<div> <div>
<Label htmlFor="signature" className="text-slate-500"> <Label htmlFor="signature" className="text-muted-foreground">
Signature Signature
</Label> </Label>

View File

@ -3,7 +3,6 @@
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
@ -11,6 +10,7 @@ import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -22,7 +22,7 @@ export const ZResetPasswordFormSchema = z
}) })
.refine((data) => data.password === data.repeatedPassword, { .refine((data) => data.password === data.repeatedPassword, {
path: ['repeatedPassword'], path: ['repeatedPassword'],
message: "Password don't match", message: "Passwords don't match",
}); });
export type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>; export type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
@ -92,7 +92,7 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div> <div>
<Label htmlFor="password" className="flex justify-between text-slate-500"> <Label htmlFor="password" className="text-muted-foreground">
<span>Password</span> <span>Password</span>
</Label> </Label>
@ -106,13 +106,11 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
{...register('password')} {...register('password')}
/> />
{errors.password && ( <FormErrorMessage className="mt-1.5" error={errors.password} />
<span className="mt-1 text-xs text-red-500">{errors.password.message}</span>
)}
</div> </div>
<div> <div>
<Label htmlFor="repeatedPassword" className="flex justify-between text-slate-500"> <Label htmlFor="repeatedPassword" className="text-muted-foreground">
<span>Repeat Password</span> <span>Repeat Password</span>
</Label> </Label>
@ -126,13 +124,10 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
{...register('repeatedPassword')} {...register('repeatedPassword')}
/> />
{errors.repeatedPassword && ( <FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
<span className="mt-1 text-xs text-red-500">{errors.repeatedPassword.message}</span>
)}
</div> </div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90"> <Button size="lg" loading={isSubmitting}>
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
Reset Password Reset Password
</Button> </Button>
</form> </form>

View File

@ -2,7 +2,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -15,6 +14,7 @@ import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -114,21 +114,18 @@ export const SignInForm = ({ className }: SignInFormProps) => {
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div> <div>
<Label htmlFor="email" className="text-slate-500"> <Label htmlFor="email" className="text-muted-forground">
Email Email
</Label> </Label>
<Input id="email" type="email" className="bg-background mt-2" {...register('email')} /> <Input id="email" type="email" className="bg-background mt-2" {...register('email')} />
{errors.email && <span className="mt-1 text-xs text-red-500">{errors.email.message}</span>} <FormErrorMessage className="mt-1.5" error={errors.email} />
</div> </div>
<div> <div>
<Label htmlFor="password" className="flex justify-between text-slate-500"> <Label htmlFor="password" className="text-muted-forground">
<span>Password</span> <span>Password</span>
<Link className="text-xs text-slate-500" href="/forgot-password">
Forgot?
</Link>
</Label> </Label>
<Input <Input
@ -141,9 +138,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
{...register('password')} {...register('password')}
/> />
{errors.password && ( <FormErrorMessage className="mt-1.5" error={errors.password} />
<span className="mt-1 text-xs text-red-500">{errors.password.message}</span>
)}
</div> </div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90"> <Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">

View File

@ -68,7 +68,7 @@ export const ResetPasswordTemplate = ({
<Section> <Section>
<Text className="my-4 text-base font-semibold"> <Text className="my-4 text-base font-semibold">
Hi, {userName}{' '} Hi, {userName}{' '}
<Link className="font-normal text-slate-400" href="mailto:{userEmail}"> <Link className="font-normal text-slate-400" href={`mailto:${userEmail}`}>
({userEmail}) ({userEmail})
</Link> </Link>
</Text> </Text>

View File

@ -29,8 +29,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
} }
const token = user.PasswordResetToken[0].token; const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const resetPasswordLink = `${process.env.NEXT_PUBLIC_SITE_URL}/reset-password/${token}`; const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, { const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl, assetBaseUrl,

View File

@ -10,19 +10,15 @@ export interface SendResetPasswordOptions {
} }
export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => { export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => {
// TODO: Better Error Handling
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
where: { where: {
id: userId, id: userId,
}, },
}); });
if (!user) { const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
throw new Error('User not found');
}
const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; console.log({ assetBaseUrl });
const template = createElement(ResetPasswordTemplate, { const template = createElement(ResetPasswordTemplate, {
assetBaseUrl, assetBaseUrl,

View File

@ -3,54 +3,51 @@ import crypto from 'crypto';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema'; import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
import { ONE_DAY, ONE_HOUR } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password'; import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => { export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
let user; const user = await prisma.user.findFirst({
try { where: {
user = await prisma.user.findFirstOrThrow({ email: {
where: { equals: email,
email: email.toLowerCase(), mode: 'insensitive',
}, },
}); },
} catch (error) { });
throw new Error('No account found with that email address.');
}
if (!user) { if (!user) {
throw new Error('No account found with that email address.'); return;
} }
// Find a token that was created in the last day and hasn't expired
const existingToken = await prisma.passwordResetToken.findFirst({ const existingToken = await prisma.passwordResetToken.findFirst({
where: { where: {
userId: user.id, userId: user.id,
expiry: {
lt: new Date(),
},
createdAt: { createdAt: {
gte: new Date(Date.now() - 1000 * 60 * 60), gt: new Date(Date.now() - ONE_HOUR),
}, },
}, },
}); });
if (existingToken) { if (existingToken) {
throw new Error('A password reset email has been sent.'); return;
} }
const token = crypto.randomBytes(64).toString('hex'); const token = crypto.randomBytes(18).toString('hex');
const expiry = new Date();
expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now
try { await prisma.passwordResetToken.create({
await prisma.passwordResetToken.create({ data: {
data: { token,
token, expiry: new Date(Date.now() + ONE_DAY),
expiry, userId: user.id,
userId: user.id, },
},
});
} catch (error) {
throw new Error('We were unable to send your email. Please try again.');
}
return await sendForgotPassword({
userId: user.id,
}); });
await sendForgotPassword({
userId: user.id,
}).catch((err) => console.error(err));
}; };

View File

@ -0,0 +1,18 @@
import { prisma } from '@documenso/prisma';
type GetResetTokenValidityOptions = {
token: string;
};
export const getResetTokenValidity = async ({ token }: GetResetTokenValidityOptions) => {
const found = await prisma.passwordResetToken.findFirst({
select: {
id: true,
},
where: {
token,
},
});
return !!found;
};

View File

@ -15,7 +15,7 @@ export const resetPassword = async ({ token, password }: ResetPasswordOptions) =
throw new Error('Invalid token provided. Please try again.'); throw new Error('Invalid token provided. Please try again.');
} }
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({ const foundToken = await prisma.passwordResetToken.findFirst({
where: { where: {
token, token,
}, },
@ -34,7 +34,7 @@ export const resetPassword = async ({ token, password }: ResetPasswordOptions) =
throw new Error('Token has expired. Please try again.'); throw new Error('Token has expired. Please try again.');
} }
const isSamePassword = await compare(password, foundToken.User.password!); const isSamePassword = await compare(password, foundToken.User.password || '');
if (isSamePassword) { if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.'); throw new Error('Your new password cannot be the same as your old password.');
@ -42,7 +42,7 @@ export const resetPassword = async ({ token, password }: ResetPasswordOptions) =
const hashedPassword = await hash(password, SALT_ROUNDS); const hashedPassword = await hash(password, SALT_ROUNDS);
const transactions = await prisma.$transaction([ await prisma.$transaction([
prisma.user.update({ prisma.user.update({
where: { where: {
id: foundToken.userId, id: foundToken.userId,
@ -58,10 +58,5 @@ export const resetPassword = async ({ token, password }: ResetPasswordOptions) =
}), }),
]); ]);
if (!transactions) {
throw new Error('We were unable to reset your password. Please try again.');
}
await sendResetPassword({ userId: foundToken.userId }); await sendResetPassword({ userId: foundToken.userId });
return transactions;
}; };

View File

@ -69,16 +69,7 @@ export const profileRouter = router({
email, email,
}); });
} catch (err) { } catch (err) {
let message = 'We were unable to send your email. Please try again.'; console.error(err);
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
} }
}), }),