mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
fix: add layout and minor updates
This commit is contained in:
@ -10,11 +10,23 @@
|
|||||||
"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"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 className="text-muted-foreground/60 mb-4 mt-2 text-sm">
|
|
||||||
Please check your email for reset instructions
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<Button asChild>
|
||||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
<Link href="/signin">Return to sign in</Link>
|
||||||
Sign in
|
</Button>
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 className="text-muted-foreground/60 mt-2 text-sm">
|
|
||||||
No worries, we'll send you reset instructions.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ForgotPasswordForm className="mt-4" />
|
<ForgotPasswordForm className="mt-4" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Don't have an account?{' '}
|
Remembered your password?{' '}
|
||||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||||
Sign up
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
27
apps/web/src/app/(unauthenticated)/layout.tsx
Normal file
27
apps/web/src/app/(unauthenticated)/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,27 +1,30 @@
|
|||||||
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 = {
|
||||||
return (
|
params: {
|
||||||
<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">
|
token: string;
|
||||||
<div className="relative flex items-center gap-x-24 md:w-[500px]">
|
};
|
||||||
<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>
|
|
||||||
|
|
||||||
|
export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) {
|
||||||
|
const isValid = await getResetTokenValidity({ token });
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
redirect('/reset-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h1 className="text-4xl font-semibold">Reset Password</h1>
|
<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>
|
<p className="text-muted-foreground mt-2 text-sm">Please choose your new password </p>
|
||||||
|
|
||||||
<ResetPasswordForm token={params.token} className="mt-4" />
|
<ResetPasswordForm token={token} className="mt-4" />
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
@ -30,7 +33,5 @@ export default function ResetPasswordPage({ params }: { params: { token: string
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 className="text-muted-foreground/60 mt-2 text-sm">
|
|
||||||
The token you provided is invalid. Please try again.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<Button className="mt-4" asChild>
|
||||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
<Link href="/signin">Return to sign in</Link>
|
||||||
Sign in
|
</Button>
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,10 @@
|
|||||||
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">
|
|
||||||
<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">
|
|
||||||
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
|
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||||
@ -32,12 +19,15 @@ export default function SignInPage() {
|
|||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden flex-1 lg:block">
|
<p className="mt-2.5 text-center">
|
||||||
<Image src={connections} alt="documenso connections" />
|
<Link
|
||||||
|
href="/forgot-password"
|
||||||
|
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||||
|
>
|
||||||
|
Forgotten your password?
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,15 @@
|
|||||||
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">
|
|
||||||
<h1 className="text-4xl font-semibold">Create a shiny, new Documenso Account ✨</h1>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||||
Create your account and start using state-of-the-art document signing. Open and
|
Create your account and start using state-of-the-art document signing. Open and beautiful
|
||||||
beautiful signing is within your grasp.
|
signing is within your grasp.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SignUpForm className="mt-4" />
|
<SignUpForm className="mt-4" />
|
||||||
@ -34,11 +21,5 @@ export default function SignUpPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden flex-1 lg:block">
|
|
||||||
<Image src={connections} alt="documenso connections" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
'A password reset email has been sent, if you have an account you should see it in your inbox shortly.',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
router.push('/check-email');
|
router.push('/check-email');
|
||||||
} catch (err) {
|
|
||||||
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
|
||||||
user = await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
where: {
|
||||||
email: email.toLowerCase(),
|
email: {
|
||||||
|
equals: email,
|
||||||
|
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,
|
expiry: new Date(Date.now() + ONE_DAY),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
throw new Error('We were unable to send your email. Please try again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sendForgotPassword({
|
await sendForgotPassword({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
}).catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
|||||||
18
packages/lib/server-only/user/get-reset-token-validity.ts
Normal file
18
packages/lib/server-only/user/get-reset-token-validity.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user