Compare commits

..

4 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan c2a9647c90 Merge branch 'feat/refresh' into feat/redirect-signed-document 2023-09-17 14:52:44 +00:00
Ephraim Atta-Duncan 02424596db fix: update for code review 2023-09-17 14:45:22 +00:00
Ephraim Atta-Duncan e47ab838c5 chore: remove undefined check 2023-09-06 07:37:03 +00:00
Ephraim Atta-Duncan 551918ab9b feat: redirect signed document to completed page 2023-09-05 13:53:18 +00:00
57 changed files with 209 additions and 2798 deletions
+17 -29
View File
@@ -1,32 +1,20 @@
{
"name": "Documenso",
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {}
},
"name": "Documenso",
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
"customizations": {
"vscode": {
"extensions": [
"aaron-bond.better-comments",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mikestead.dotenv",
"unifiedjs.vscode-mdx",
"GitHub.copilot-chat",
"GitHub.copilot-labs",
"GitHub.copilot",
"GitHub.vscode-pull-request-github",
"Prisma.prisma",
"VisualStudioExptTeam.vscodeintellicode",
]
}
}
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
]
}
+2 -2
View File
@@ -7,8 +7,8 @@ NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
# [[APP]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
-1
View File
@@ -5,4 +5,3 @@
# Statically hosted javascript files
apps/*/public/*.js
apps/*/public/*.cjs
scripts/
-6
View File
@@ -22,18 +22,12 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- name: Build
run: npm run build
+2 -5
View File
@@ -32,10 +32,7 @@ jobs:
- name: Install Dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- name: Build Documenso
run: npm run build
@@ -45,4 +42,4 @@ jobs:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v2
+2 -2
View File
@@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso.
We redesigned the whole signing flow to make it more appealing and more convenient.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
**We call it happy minimalism.**
We paid particular attention to the moment of signing, which should be celebrated.
We paid particular attention to the moment of signing, which should be celebrated.
The image below is the final bloom of the completion celebration we added:
-34
View File
@@ -18,40 +18,6 @@ const config = {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
},
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-dns-prefetch-control',
value: 'on',
},
{
key: 'strict-transport-security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'x-frame-options',
value: 'SAMEORIGIN',
},
{
key: 'x-content-type-options',
value: 'nosniff',
},
{
key: 'referrer-policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'permissions-policy',
value:
'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
},
],
},
];
},
};
module.exports = withContentlayer(config);
+1 -2
View File
@@ -1,7 +1,6 @@
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PUBLIC_SITE_URL?: string;
NEXT_PRIVATE_DATABASE_URL: string;
@@ -19,7 +19,6 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
return {
title: `Documenso - ${blogPost.title}`,
description: blogPost.description,
};
};
@@ -161,7 +161,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</p>
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`}
href={`${process.env.NEXT_PUBLIC_APP_URL}/login`}
target="_blank"
className="mt-4 block"
>
+2 -2
View File
@@ -21,12 +21,12 @@ export const metadata = {
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
type: 'website',
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`],
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`],
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
},
@@ -43,7 +43,7 @@ export default async function handler(
if (user && user.Subscription.length > 0) {
return res.status(200).json({
redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`,
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
});
}
@@ -103,8 +103,8 @@ export default async function handler(
mode: 'subscription',
metadata,
allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent(
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent(
email,
)}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`,
});
+1 -2
View File
@@ -1,7 +1,6 @@
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PUBLIC_SITE_URL?: string;
NEXT_PRIVATE_DATABASE_URL: string;
@@ -35,7 +35,7 @@ export default async function BillingSettingsPage() {
if (subscription.customerId) {
billingPortalUrl = await getPortalSession({
customerId: subscription.customerId,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,
});
}
@@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern';
@@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { FieldType } from '@documenso/prisma/client';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@@ -53,6 +53,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const user = await getServerComponentSession();
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
) {
redirect(`/sign/${token}/complete`);
}
return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
@@ -1,20 +0,0 @@
import Link from 'next/link';
import { Button } from '@documenso/ui/primitives/button';
export default function ForgotPasswordPage() {
return (
<div>
<h1 className="text-4xl font-semibold">Email sent!</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
A password reset email has been sent, if you have an account you should see it in your inbox
shortly.
</p>
<Button asChild>
<Link href="/signin">Return to sign in</Link>
</Button>
</div>
);
}
@@ -1,25 +0,0 @@
import Link from 'next/link';
import { ForgotPasswordForm } from '~/components/forms/forgot-password';
export default function ForgotPasswordPage() {
return (
<div>
<h1 className="text-4xl font-semibold">Forgotten your password?</h1>
<p className="text-muted-foreground mt-2 text-sm">
No worries, it happens! Enter your email and we'll email you a special link to reset your
password.
</p>
<ForgotPasswordForm className="mt-4" />
<p className="text-muted-foreground mt-6 text-center text-sm">
Remembered your password?{' '}
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Sign In
</Link>
</p>
</div>
);
}
@@ -1,27 +0,0 @@
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,37 +0,0 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity';
import { ResetPasswordForm } from '~/components/forms/reset-password';
type ResetPasswordPageProps = {
params: {
token: string;
};
};
export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) {
const isValid = await getResetTokenValidity({ token });
if (!isValid) {
redirect('/reset-password');
}
return (
<div className="w-full">
<h1 className="text-4xl font-semibold">Reset Password</h1>
<p className="text-muted-foreground mt-2 text-sm">Please choose your new password </p>
<ResetPasswordForm token={token} 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>
</div>
);
}
@@ -1,20 +0,0 @@
import Link from 'next/link';
import { Button } from '@documenso/ui/primitives/button';
export default function ResetPasswordPage() {
return (
<div>
<h1 className="text-4xl font-semibold">Unable to reset password</h1>
<p className="text-muted-foreground mt-2 text-sm">
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>
<Button className="mt-4" asChild>
<Link href="/signin">Return to sign in</Link>
</Button>
</div>
);
}
@@ -1,33 +1,43 @@
import Image from 'next/image';
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';
export default function SignInPage() {
return (
<div>
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
<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 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>
<p className="text-muted-foreground/60 mt-2 text-sm">
Welcome back, we are lucky to have you.
</p>
<div className="max-w-md">
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
<SignInForm className="mt-4" />
<p className="text-muted-foreground/60 mt-2 text-sm">
Welcome back, we are lucky to have you.
</p>
<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>
<SignInForm className="mt-4" />
<p className="mt-2.5 text-center">
<Link
href="/forgot-password"
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
>
Forgotten your password?
</Link>
</p>
</div>
<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>
</div>
<div className="hidden flex-1 lg:block">
<Image src={connections} alt="documenso connections" />
</div>
</div>
</main>
);
}
@@ -1,25 +1,44 @@
import Image from 'next/image';
import Link from 'next/link';
import backgroundPattern from '~/assets/background-pattern.png';
import connections from '~/assets/connections.png';
import { SignUpForm } from '~/components/forms/signup';
export default function SignUpPage() {
return (
<div>
<h1 className="text-4xl font-semibold">Create a new account</h1>
<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 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>
<p className="text-muted-foreground/60 mt-2 text-sm">
Create your account and start using state-of-the-art document signing. Open and beautiful
signing is within your grasp.
</p>
<div className="max-w-md">
<h1 className="text-4xl font-semibold">Create a shiny, new Documenso Account </h1>
<SignUpForm className="mt-4" />
<p className="text-muted-foreground/60 mt-2 text-sm">
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 mt-6 text-center text-sm">
Already have an account?{' '}
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Sign in instead
</Link>
</p>
</div>
<SignUpForm className="mt-4" />
<p className="text-muted-foreground mt-6 text-center text-sm">
Already have an account?{' '}
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
Sign in instead
</Link>
</p>
</div>
<div className="hidden flex-1 lg:block">
<Image src={connections} alt="documenso connections" />
</div>
</div>
</main>
);
}
+2 -2
View File
@@ -33,12 +33,12 @@ export const metadata = {
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
type: 'website',
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`],
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`],
images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`],
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
},
@@ -1,80 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
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 { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZForgotPasswordFormSchema = z.object({
email: z.string().email().min(1),
});
export type TForgotPasswordFormSchema = z.infer<typeof ZForgotPasswordFormSchema>;
export type ForgotPasswordFormProps = {
className?: string;
};
export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
const router = useRouter();
const { toast } = useToast();
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<TForgotPasswordFormSchema>({
values: {
email: '',
},
resolver: zodResolver(ZForgotPasswordFormSchema),
});
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
await forgotPassword({ email }).catch(() => null);
toast({
title: 'Reset email sent',
description:
'A password reset email has been sent, if you have an account you should see it in your inbox shortly.',
duration: 5000,
});
reset();
router.push('/check-email');
};
return (
<form
className={cn('flex w-full flex-col gap-y-4', className)}
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="email" className="text-muted-foreground">
Email
</Label>
<Input id="email" type="email" className="bg-background mt-2" {...register('email')} />
<FormErrorMessage className="mt-1.5" error={errors.email} />
</div>
<Button size="lg" loading={isSubmitting}>
Reset Password
</Button>
</form>
);
};
+2 -2
View File
@@ -88,7 +88,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="password" className="text-muted-foreground">
<Label htmlFor="password" className="text-slate-500">
Password
</Label>
@@ -106,7 +106,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
</div>
<div>
<Label htmlFor="repeated-password" className="text-muted-foreground">
<Label htmlFor="repeated-password" className="text-slate-500">
Repeat Password
</Label>
+3 -3
View File
@@ -89,7 +89,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="full-name" className="text-muted-foreground">
<Label htmlFor="full-name" className="text-slate-500">
Full Name
</Label>
@@ -99,7 +99,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</div>
<div>
<Label htmlFor="email" className="text-muted-foreground">
<Label htmlFor="email" className="text-slate-500">
Email
</Label>
@@ -107,7 +107,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</div>
<div>
<Label htmlFor="signature" className="text-muted-foreground">
<Label htmlFor="signature" className="text-slate-500">
Signature
</Label>
@@ -1,135 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
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 { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZResetPasswordFormSchema = z
.object({
password: z.string().min(6).max(72),
repeatedPassword: z.string().min(6).max(72),
})
.refine((data) => data.password === data.repeatedPassword, {
path: ['repeatedPassword'],
message: "Passwords don't match",
});
export type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
export type ResetPasswordFormProps = {
className?: string;
token: string;
};
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
const router = useRouter();
const { toast } = useToast();
const {
register,
reset,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TResetPasswordFormSchema>({
values: {
password: '',
repeatedPassword: '',
},
resolver: zodResolver(ZResetPasswordFormSchema),
});
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
try {
await resetPassword({
password,
token,
});
reset();
toast({
title: 'Password updated',
description: 'Your password has been updated successfully.',
duration: 5000,
});
router.push('/signin');
} 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 reset your password. Please try again later.',
});
}
}
};
return (
<form
className={cn('flex w-full flex-col gap-y-4', className)}
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="password" className="text-muted-foreground">
<span>Password</span>
</Label>
<Input
id="password"
type="password"
minLength={6}
maxLength={72}
autoComplete="current-password"
className="bg-background mt-2"
{...register('password')}
/>
<FormErrorMessage className="mt-1.5" error={errors.password} />
</div>
<div>
<Label htmlFor="repeatedPassword" className="text-muted-foreground">
<span>Repeat Password</span>
</Label>
<Input
id="repeatedPassword"
type="password"
minLength={6}
maxLength={72}
autoComplete="current-password"
className="bg-background mt-2"
{...register('repeatedPassword')}
/>
<FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
</div>
<Button size="lg" loading={isSubmitting}>
Reset Password
</Button>
</form>
);
};
+7 -6
View File
@@ -14,7 +14,6 @@ import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { cn } from '@documenso/ui/lib/utils';
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 { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -114,18 +113,18 @@ export const SignInForm = ({ className }: SignInFormProps) => {
onSubmit={handleSubmit(onFormSubmit)}
>
<div>
<Label htmlFor="email" className="text-muted-forground">
<Label htmlFor="email" className="text-slate-500">
Email
</Label>
<Input id="email" type="email" className="bg-background mt-2" {...register('email')} />
<FormErrorMessage className="mt-1.5" error={errors.email} />
{errors.email && <span className="mt-1 text-xs text-red-500">{errors.email.message}</span>}
</div>
<div>
<Label htmlFor="password" className="text-muted-forground">
<span>Password</span>
<Label htmlFor="password" className="text-slate-500">
Password
</Label>
<Input
@@ -138,7 +137,9 @@ export const SignInForm = ({ className }: SignInFormProps) => {
{...register('password')}
/>
<FormErrorMessage className="mt-1.5" error={errors.password} />
{errors.password && (
<span className="mt-1 text-xs text-red-500">{errors.password.message}</span>
)}
</div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">
+2 -2
View File
@@ -21,7 +21,7 @@ export const getFlag = async (
return LOCAL_FEATURE_FLAGS[flag] ?? true;
}
const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/get`);
const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
const response = await fetch(url, {
@@ -54,7 +54,7 @@ export const getAllFlags = async (
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/all`);
const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/all`);
return fetch(url, {
headers: {
+3 -3
View File
@@ -43,7 +43,7 @@ export default async function handler(
if (user && user.Subscription.length > 0) {
return res.status(200).json({
redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`,
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
});
}
@@ -103,8 +103,8 @@ export default async function handler(
mode: 'subscription',
metadata,
allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent(
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent(
email,
)}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`,
});
Binary file not shown.
+42 -1348
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -17,12 +17,11 @@
},
"dependencies": {
"@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3",
"react-email": "^1.9.4"
"nodemailer": "^6.9.3"
},
"devDependencies": {
"@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*",
"@documenso/tailwind-config": "*",
"@types/nodemailer": "^6.4.8",
"tsup": "^7.1.0"
}
@@ -1,20 +1,14 @@
import { Link, Section, Text } from '@react-email/components';
export type TemplateFooterProps = {
isDocument?: boolean;
};
export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
export const TemplateFooter = () => {
return (
<Section>
{isDocument && (
<Text className="my-4 text-base text-slate-400">
This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documenso.com">
Documenso.
</Link>
</Text>
)}
<Text className="my-4 text-base text-slate-400">
This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documenso.com">
Documenso.
</Link>
</Text>
<Text className="my-8 text-sm text-slate-400">
Documenso
@@ -1,54 +0,0 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
export type TemplateForgotPasswordProps = {
resetPasswordLink: string;
assetBaseUrl: string;
};
export const TemplateForgotPassword = ({
resetPasswordLink,
assetBaseUrl,
}: TemplateForgotPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Section className="mt-4 flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Forgot your password?
</Text>
<Text className="my-1 text-center text-base text-slate-400">
That's okay, it happens! Click the button below to reset your password.
</Text>
<Section className="mb-6 mt-8 text-center">
<Button
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={resetPasswordLink}
>
Reset Password
</Button>
</Section>
</Section>
</Tailwind>
);
};
export default TemplateForgotPassword;
@@ -1,43 +0,0 @@
import { Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
export interface TemplateResetPasswordProps {
userName: string;
userEmail: string;
assetBaseUrl: string;
}
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Section className="mt-4 flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Password updated!
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Your password has been updated.
</Text>
</Section>
</Tailwind>
);
};
export default TemplateResetPassword;
@@ -1,74 +0,0 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer';
import {
TemplateForgotPassword,
TemplateForgotPasswordProps,
} from '../template-components/template-forgot-password';
export type ForgotPasswordTemplateProps = Partial<TemplateForgotPasswordProps>;
export const ForgotPasswordTemplate = ({
resetPasswordLink = 'https://documenso.com',
assetBaseUrl = 'http://localhost:3002',
}: ForgotPasswordTemplateProps) => {
const previewText = `Password Reset Requested`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<TemplateForgotPassword
resetPasswordLink={resetPasswordLink}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<div className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
</Html>
);
};
export default ForgotPasswordTemplate;
-102
View File
@@ -1,102 +0,0 @@
import {
Body,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from '@react-email/components';
import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer';
import {
TemplateResetPassword,
TemplateResetPasswordProps,
} from '../template-components/template-reset-password';
export type ResetPasswordTemplateProps = Partial<TemplateResetPasswordProps>;
export const ResetPasswordTemplate = ({
userName = 'Lucas Smith',
userEmail = 'lucas@documenso.com',
assetBaseUrl = 'http://localhost:3002',
}: ResetPasswordTemplateProps) => {
const previewText = `Password Reset Successful`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<TemplateResetPassword
userName={userName}
userEmail={userEmail}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Container className="mx-auto mt-12 max-w-xl">
<Section>
<Text className="my-4 text-base font-semibold">
Hi, {userName}{' '}
<Link className="font-normal text-slate-400" href={`mailto:${userEmail}`}>
({userEmail})
</Link>
</Text>
<Text className="mt-2 text-base text-slate-400">
We've changed your password as you asked. You can now sign in with your new
password.
</Text>
<Text className="mt-2 text-base text-slate-400">
Didn't request a password change? We are here to help you secure your account,
just{' '}
<Link className="text-documenso-700 font-normal" href="mailto:hi@documenso.com">
contact us.
</Link>
</Text>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
</Html>
);
};
export default ResetPasswordTemplate;
@@ -1,53 +0,0 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
import { prisma } from '@documenso/prisma';
export interface SendForgotPasswordOptions {
userId: number;
}
export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
include: {
PasswordResetToken: {
orderBy: {
createdAt: 'desc',
},
take: 1,
},
},
});
if (!user) {
throw new Error('User not found');
}
const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl,
resetPasswordLink,
});
return await mailer.sendMail({
to: {
address: user.email,
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Forgot Password?',
html: render(template),
text: render(template, { plainText: true }),
});
};
@@ -1,42 +0,0 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
import { prisma } from '@documenso/prisma';
export interface SendResetPasswordOptions {
userId: number;
}
export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
console.log({ assetBaseUrl });
const template = createElement(ResetPasswordTemplate, {
assetBaseUrl,
userEmail: user.email,
userName: user.name || '',
});
return await mailer.sendMail({
to: {
address: user.email,
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Password Reset Success!',
html: render(template),
text: render(template, { plainText: true }),
});
};
@@ -48,8 +48,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
return;
}
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_SITE_URL}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
@@ -1,53 +0,0 @@
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
import { ONE_DAY, ONE_HOUR } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
const user = await prisma.user.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
});
if (!user) {
return;
}
// Find a token that was created in the last hour and hasn't expired
const existingToken = await prisma.passwordResetToken.findFirst({
where: {
userId: user.id,
expiry: {
gt: new Date(),
},
createdAt: {
gt: new Date(Date.now() - ONE_HOUR),
},
},
});
if (existingToken) {
return;
}
const token = crypto.randomBytes(18).toString('hex');
await prisma.passwordResetToken.create({
data: {
token,
expiry: new Date(Date.now() + ONE_DAY),
userId: user.id,
},
});
await sendForgotPassword({
userId: user.id,
}).catch((err) => console.error(err));
};
@@ -1,19 +0,0 @@
import { prisma } from '@documenso/prisma';
type GetResetTokenValidityOptions = {
token: string;
};
export const getResetTokenValidity = async ({ token }: GetResetTokenValidityOptions) => {
const found = await prisma.passwordResetToken.findFirst({
select: {
id: true,
expiry: true,
},
where: {
token,
},
});
return !!found && found.expiry > new Date();
};
@@ -1,62 +0,0 @@
import { compare, hash } from 'bcrypt';
import { prisma } from '@documenso/prisma';
import { SALT_ROUNDS } from '../../constants/auth';
import { sendResetPassword } from '../auth/send-reset-password';
export type ResetPasswordOptions = {
token: string;
password: string;
};
export const resetPassword = async ({ token, password }: ResetPasswordOptions) => {
if (!token) {
throw new Error('Invalid token provided. Please try again.');
}
const foundToken = await prisma.passwordResetToken.findFirst({
where: {
token,
},
include: {
User: true,
},
});
if (!foundToken) {
throw new Error('Invalid token provided. Please try again.');
}
const now = new Date();
if (now > foundToken.expiry) {
throw new Error('Token has expired. Please try again.');
}
const isSamePassword = await compare(password, foundToken.User.password || '');
if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.');
}
const hashedPassword = await hash(password, SALT_ROUNDS);
await prisma.$transaction([
prisma.user.update({
where: {
id: foundToken.userId,
},
data: {
password: hashedPassword,
},
}),
prisma.passwordResetToken.deleteMany({
where: {
userId: foundToken.userId,
},
}),
]);
await sendResetPassword({ userId: foundToken.userId });
};
+2 -2
View File
@@ -8,8 +8,8 @@ export const getBaseUrl = () => {
return `https://${process.env.VERCEL_URL}`;
}
if (process.env.NEXT_PUBLIC_WEBAPP_URL) {
return process.env.NEXT_PUBLIC_WEBAPP_URL;
if (process.env.NEXT_PUBLIC_SITE_URL) {
return `https://${process.env.NEXT_PUBLIC_SITE_URL}`;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
-52
View File
@@ -1,52 +0,0 @@
/// <reference types="@documenso/tsconfig/process-env.d.ts" />
export const getDatabaseUrl = () => {
if (process.env.NEXT_PRIVATE_DATABASE_URL) {
return process.env.NEXT_PRIVATE_DATABASE_URL;
}
if (process.env.POSTGRES_URL) {
process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL;
process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL;
}
if (process.env.DATABASE_URL) {
process.env.NEXT_PRIVATE_DATABASE_URL = process.env.DATABASE_URL;
process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.DATABASE_URL;
}
if (process.env.POSTGRES_PRISMA_URL) {
process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_PRISMA_URL;
}
if (process.env.POSTGRES_URL_NON_POOLING) {
process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING;
}
// We change the protocol from `postgres:` to `https:` so we can construct a easily
// mofifiable URL.
const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL.replace('postgres://', 'https://'));
// If we're using a connection pool, we need to let Prisma know that
// we're using PgBouncer.
if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) {
url.searchParams.set('pgbouncer', 'true');
process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://');
}
// Support for neon.tech (Neon Database)
if (url.hostname.endsWith('neon.tech')) {
const [projectId, ...rest] = url.hostname.split('.');
if (!projectId.endsWith('-pooler')) {
url.hostname = `${projectId}-pooler.${rest.join('.')}`;
}
url.searchParams.set('pgbouncer', 'true');
process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://');
}
return process.env.NEXT_PRIVATE_DATABASE_URL;
};
+2 -8
View File
@@ -1,7 +1,5 @@
import { PrismaClient } from '@prisma/client';
import { getDatabaseUrl } from './helper';
declare global {
// We need `var` to declare a global variable in TypeScript
// eslint-disable-next-line no-var
@@ -9,13 +7,9 @@ declare global {
}
if (!globalThis.prisma) {
globalThis.prisma = new PrismaClient({ datasourceUrl: getDatabaseUrl() });
globalThis.prisma = new PrismaClient();
}
export const prisma =
globalThis.prisma ||
new PrismaClient({
datasourceUrl: getDatabaseUrl(),
});
export const prisma = globalThis.prisma || new PrismaClient();
export const getPrismaClient = () => prisma;
@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiry" TIMESTAMP(3) NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
-- AddForeignKey
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+3 -11
View File
@@ -9,18 +9,10 @@
"format": "prisma format",
"prisma:generate": "prisma generate",
"prisma:migrate-dev": "prisma migrate dev",
"prisma:migrate-deploy": "prisma migrate deploy",
"prisma:seed": "prisma db seed"
},
"prisma": {
"seed": "ts-node --transpileOnly --skipProject ./seed-database.ts"
"prisma:migrate-deploy": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "5.3.1",
"prisma": "5.3.1"
},
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
"@prisma/client": "5.0.0",
"prisma": "5.0.0"
}
}
+13 -23
View File
@@ -19,29 +19,19 @@ enum Role {
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
emailVerified DateTime?
password String?
source String?
signature String?
roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO)
accounts Account[]
sessions Session[]
Document Document[]
Subscription Subscription[]
PasswordResetToken PasswordResetToken[]
}
model PasswordResetToken {
id Int @id @default(autoincrement())
token String @unique
createdAt DateTime @default(now())
expiry DateTime
userId Int
User User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
name String?
email String @unique
emailVerified DateTime?
password String?
source String?
signature String?
roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO)
accounts Account[]
sessions Session[]
Document Document[]
Subscription Subscription[]
}
enum SubscriptionStatus {
-82
View File
@@ -1,82 +0,0 @@
import { DocumentDataType, Role } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import { hashSync } from '@documenso/lib/server-only/auth/hash';
import { prisma } from './index';
const seedDatabase = async () => {
const examplePdf = fs
.readFileSync(path.join(__dirname, '../../assets/example.pdf'))
.toString('base64');
const exampleUser = await prisma.user.upsert({
where: {
email: 'example@documenso.com',
},
create: {
name: 'Example User',
email: 'example@documenso.com',
password: hashSync('password'),
roles: [Role.USER],
},
update: {},
});
const adminUser = await prisma.user.upsert({
where: {
email: 'admin@documenso.com',
},
create: {
name: 'Admin User',
email: 'admin@documenso.com',
password: hashSync('password'),
roles: [Role.USER, Role.ADMIN],
},
update: {},
});
const examplePdfData = await prisma.documentData.upsert({
where: {
id: 'clmn0kv5k0000pe04vcqg5zla',
},
create: {
id: 'clmn0kv5k0000pe04vcqg5zla',
type: DocumentDataType.BYTES_64,
data: examplePdf,
initialData: examplePdf,
},
update: {},
});
await prisma.document.upsert({
where: {
id: 1,
},
create: {
id: 1,
title: 'Example Document',
documentDataId: examplePdfData.id,
userId: exampleUser.id,
Recipient: {
create: {
name: String(adminUser.name),
email: adminUser.email,
token: Math.random().toString(36).slice(2, 9),
},
},
},
update: {},
});
};
seedDatabase()
.then(() => {
console.log('Database seeded');
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});
+2 -43
View File
@@ -1,17 +1,10 @@
import { TRPCError } from '@trpc/server';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZForgotPasswordFormSchema,
ZResetPasswordFormSchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
import { authenticatedProcedure, router } from '../trpc';
import { ZUpdatePasswordMutationSchema, ZUpdateProfileMutationSchema } from './schema';
export const profileRouter = router({
updateProfile: authenticatedProcedure
@@ -60,38 +53,4 @@ export const profileRouter = router({
});
}
}),
forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => {
try {
const { email } = input;
return await forgotPassword({
email,
});
} catch (err) {
console.error(err);
}
}),
resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input }) => {
try {
const { password, token } = input;
return await resetPassword({
token,
password,
});
} catch (err) {
let message = 'We were unable to reset your password. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}),
});
+2 -12
View File
@@ -5,20 +5,10 @@ export const ZUpdateProfileMutationSchema = z.object({
signature: z.string(),
});
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
export const ZUpdatePasswordMutationSchema = z.object({
password: z.string().min(6),
});
export const ZForgotPasswordFormSchema = z.object({
email: z.string().email().min(1),
});
export const ZResetPasswordFormSchema = z.object({
password: z.string().min(6),
token: z.string().min(1),
});
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
export type TUpdatePasswordMutationSchema = z.infer<typeof ZUpdatePasswordMutationSchema>;
export type TForgotPasswordFormSchema = z.infer<typeof ZForgotPasswordFormSchema>;
export type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
+1 -16
View File
@@ -1,7 +1,6 @@
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PUBLIC_SITE_URL?: string;
NEXT_PRIVATE_GOOGLE_CLIENT_ID?: string;
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET?: string;
@@ -41,19 +40,5 @@ declare namespace NodeJS {
NEXT_PRIVATE_SMTP_FROM_NAME?: string;
NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string;
/**
* Vercel environment variables
*/
VERCEL?: string;
VERCEL_ENV?: 'production' | 'development' | 'preview';
VERCEL_URL?: string;
DEPLOYMENT_TARGET?: 'webapp' | 'marketing';
POSTGRES_URL?: string;
DATABASE_URL?: string;
POSTGRES_PRISMA_URL?: string;
POSTGRES_URL_NON_POOLING?: string;
}
}
-45
View File
@@ -1,45 +0,0 @@
/** @typedef {import('@documenso/tsconfig/process-env')} */
/**
* Remap Vercel environment variables to our defined Next.js environment variables.
*
* @deprecated This is no longer needed because we can't inject runtime environment variables via next.config.js
*
* @returns {void}
*/
const remapVercelEnv = () => {
if (!process.env.VERCEL || !process.env.DEPLOYMENT_TARGET) {
return;
}
if (process.env.POSTGRES_URL) {
process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL;
}
if (process.env.POSTGRES_URL_NON_POOLING) {
process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING;
}
// If we're using a connection pool, we need to let Prisma know that
// we're using PgBouncer.
if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) {
const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL);
url.searchParams.set('pgbouncer', 'true');
process.env.NEXT_PRIVATE_DATABASE_URL = url.toString();
}
if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'webapp') {
process.env.NEXTAUTH_URL = `https://${process.env.VERCEL_URL}`;
process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`;
}
if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'marketing') {
process.env.NEXT_PUBLIC_MARKETING_URL = `https://${process.env.VERCEL_URL}`;
}
};
module.exports = {
remapVercelEnv,
};
-107
View File
@@ -1,107 +0,0 @@
#!/usr/bin/env bash
# Exit on error.
set -eo pipefail
# Get the directory of this script, regardless of where it is called from.
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
function log() {
echo "[VercelBuild]: $1"
}
function build_webapp() {
log "Building webapp for $VERCEL_ENV"
remap_database_integration
npm run prisma:generate --workspace=@documenso/prisma
npm run prisma:migrate-deploy --workspace=@documenso/prisma
if [[ "$VERCEL_ENV" != "production" ]]; then
log "Seeding database for $VERCEL_ENV"
npm run prisma:seed --workspace=@documenso/prisma
fi
npm run build -- --filter @documenso/web
}
function remap_webapp_env() {
if [[ "$VERCEL_ENV" != "production" ]]; then
log "Remapping webapp environment variables for $VERCEL_ENV"
export NEXTAUTH_URL="https://$VERCEL_URL"
export NEXT_PUBLIC_WEBAPP_URL="https://$VERCEL_URL"
fi
}
function build_marketing() {
log "Building marketing for $VERCEL_ENV"
remap_database_integration
npm run prisma:generate --workspace=@documenso/prisma
npm run build -- --filter @documenso/marketing
}
function remap_marketing_env() {
if [[ "$VERCEL_ENV" != "production" ]]; then
log "Remapping marketing environment variables for $VERCEL_ENV"
export NEXT_PUBLIC_MARKETING_URL="https://$VERCEL_URL"
fi
}
function remap_database_integration() {
log "Remapping Supabase integration for $VERCEL_ENV"
if [[ ! -z "$POSTGRES_URL" ]]; then
export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL"
export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL"
fi
if [[ ! -z "$DATABASE_URL" ]]; then
export NEXT_PRIVATE_DATABASE_URL="$DATABASE_URL"
export NEXT_PRIVATE_DIRECT_DATABASE_URL="$DATABASE_URL"
fi
if [[ ! -z "$POSTGRES_URL_NON_POOLING" ]]; then
export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL?pgbouncer=true"
export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL_NON_POOLING"
fi
if [[ "$NEXT_PRIVATE_DATABASE_URL" == *"neon.tech"* ]]; then
log "Remapping for Neon integration"
PROJECT_ID="$(echo "$PGHOST" | cut -d'.' -f1)"
PGBOUNCER_HOST="$(echo "$PGHOST" | sed "s/${PROJECT_ID}/${PROJECT_ID}-pooler/")"
export NEXT_PRIVATE_DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@${PGBOUNCER_HOST}/${PGDATABASE}?pgbouncer=true"
fi
}
# Navigate to the root of the project.
cd "$SCRIPT_DIR/.."
# Check if the script is running on Vercel.
if [[ -z "$VERCEL" ]]; then
log "ERROR - This script must be run as part of the Vercel build process."
exit 1
fi
case "$DEPLOYMENT_TARGET" in
"webapp")
build_webapp
;;
"marketing")
build_marketing
;;
*)
log "ERROR - Missing or invalid DEPLOYMENT_TARGET environment variable."
log "ERROR - DEPLOYMENT_TARGET must be either 'webapp' or 'marketing'."
exit 1
;;
esac
+7 -23
View File
@@ -2,13 +2,8 @@
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
".next/**",
"!.next/cache/**"
]
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {},
"dev": {
@@ -16,22 +11,20 @@
"persistent": true
}
},
"globalDependencies": [
"**/.env.*local"
],
"globalDependencies": ["**/.env.*local"],
"globalEnv": [
"APP_VERSION",
"NEXTAUTH_URL",
"NEXTAUTH_SECRET",
"NEXT_PUBLIC_WEBAPP_URL",
"NEXT_PUBLIC_MARKETING_URL",
"NEXT_PUBLIC_APP_URL",
"NEXT_PUBLIC_SITE_URL",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
"NEXT_PRIVATE_NEXT_AUTH_SECRET",
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
"NEXT_PRIVATE_GOOGLE_CLIENT_SECRET",
"NEXT_PUBLIC_UPLOAD_TRANSPORT",
@@ -55,15 +48,6 @@
"NEXT_PRIVATE_SMTP_SECURE",
"NEXT_PRIVATE_SMTP_FROM_NAME",
"NEXT_PRIVATE_SMTP_FROM_ADDRESS",
"NEXT_PRIVATE_STRIPE_API_KEY",
"VERCEL",
"VERCEL_ENV",
"VERCEL_URL",
"DEPLOYMENT_TARGET",
"POSTGRES_URL",
"DATABASE_URL",
"POSTGRES_PRISMA_URL",
"POSTGRES_URL_NON_POOLING"
"NEXT_PRIVATE_STRIPE_API_KEY"
]
}