Compare commits
16 Commits
chore/docu
...
v1.6.1-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b8914da83 | ||
|
|
29910ab2a7 | ||
|
|
ef3ecc33f1 | ||
|
|
0244f021ab | ||
|
|
e5f73452b3 | ||
|
|
c605877924 | ||
|
|
e0065a8731 | ||
|
|
f74265850b | ||
|
|
909c38f47e | ||
|
|
1beb434a72 | ||
|
|
5582f29bda | ||
|
|
7ed0a909eb | ||
|
|
a9025b5d97 | ||
|
|
0c744a1123 | ||
|
|
0f86eed6ac | ||
|
|
4b485268ca |
@@ -10,12 +10,19 @@ NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
||||
|
||||
# [[AUTH OPTIONAL]]
|
||||
# Find documentation on setting up Google OAuth here:
|
||||
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
||||
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
|
||||
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
|
||||
# This can be used to still allow signups for OIDC connections
|
||||
# when signup is disabled via `NEXT_PUBLIC_DISABLE_SIGNUP`
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP=""
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
||||
|
||||
# [[URLS]]
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"index": "Getting Started",
|
||||
"signing-certificate": "Signing Certificate",
|
||||
"how-to": "How To"
|
||||
}
|
||||
"how-to": "How To",
|
||||
"setting-up-oauth-providers": "Setting up OAuth Providers"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Setting up OAuth Providers
|
||||
description: Learn how to set up OAuth providers for your own instance of Documenso.
|
||||
---
|
||||
|
||||
## Google OAuth (Gmail)
|
||||
|
||||
To use Google OAuth, you will need to create a Google Cloud Platform project and enable the Google Identity and Access Management (IAM) API. You will also need to create a new OAuth client ID and download the client secret.
|
||||
|
||||
### Create and configure a new OAuth client ID
|
||||
|
||||
1. Go to the [Google Cloud Platform Console](https://console.cloud.google.com/)
|
||||
2. From the projects list, select a project or create a new one
|
||||
3. If the APIs & services page isn't already open, open the console left side menu and select APIs & services
|
||||
4. On the left, click Credentials
|
||||
5. Click New Credentials, then select OAuth client ID
|
||||
6. When prompted to select an application type, select Web application
|
||||
7. Enter a name for your client ID, and click Create
|
||||
8. Click the download button to download the client secret
|
||||
9. Set the authorized javascript origins to `https://<documenso-domain>`
|
||||
10. Set the authorized redirect URIs to `https://<documenso-domain>/api/auth/callback/google`
|
||||
11. In the Documenso environment variables, set the following:
|
||||
|
||||
```
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=<your-client-id>
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=<your-client-secret>
|
||||
```
|
||||
|
||||
Finally verify the signing in with Google works by signing in with your Google account and checking the email address in your profile.
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"index": "Introduction",
|
||||
"support": "Support",
|
||||
"-- How To Use": {
|
||||
"type": "separator",
|
||||
"title": "How To Use"
|
||||
@@ -13,6 +14,7 @@
|
||||
"type": "separator",
|
||||
"title": "Legal Overview"
|
||||
},
|
||||
"fair-use": "Fair Use Policy",
|
||||
"licenses": "Licenses",
|
||||
"compliance": "Compliance"
|
||||
}
|
||||
|
||||
34
apps/documentation/pages/users/fair-use.mdx
Normal file
34
apps/documentation/pages/users/fair-use.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Fair Use Policy
|
||||
description: Learn about our fair use policy, which enables us to have unlimited plans.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Fair Use Policy
|
||||
|
||||
### Why
|
||||
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using. This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
|
||||
### Spirit of the Plan
|
||||
|
||||
> Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
|
||||
<Callout type="info">
|
||||
What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
|
||||
pricing. We won’t block your account without reaching out. [Message
|
||||
us](mailto:support@documenso.com) for questions. It's probably fine, though.
|
||||
</Callout>
|
||||
|
||||
### DO
|
||||
|
||||
- Sign as many documents with the individual plan for your single business or organization you are part of
|
||||
- Use the API and Zapier to automate all your signing to sign as much as possible
|
||||
- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use the individual account's API to power a platform
|
||||
- Run a huge company, signing thousands of documents per day on a two-user team plan using the API
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win, and it's probably fine
|
||||
@@ -3,7 +3,7 @@ title: Create Your Account
|
||||
description: Learn how to create an account on Documenso.
|
||||
---
|
||||
|
||||
import { Steps } from 'nextra/components';
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Create Your Account
|
||||
|
||||
@@ -14,6 +14,8 @@ The first step to start using Documenso is to pick a plan and create an account.
|
||||
|
||||
Explore each plan's features and choose the one that best suits your needs. The [pricing page](https://documen.so/pricing) has more information about the plans.
|
||||
|
||||
<Callout>All plans are subject to our [Fair Use Policy](/users/fair-use).</Callout>
|
||||
|
||||
### Create an account
|
||||
|
||||
If you are unsure which plan to choose, you can start with the free plan and upgrade later.
|
||||
|
||||
38
apps/documentation/pages/users/support.mdx
Normal file
38
apps/documentation/pages/users/support.mdx
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Support
|
||||
description: Learn what types of support we offer.
|
||||
---
|
||||
|
||||
# Support
|
||||
|
||||
## Community Support
|
||||
|
||||
If you are a developer or free user, you can reach out to the community or raise an issue:
|
||||
|
||||
### [Create Github Issues](https://github.com/documenso/documenso/issues)
|
||||
|
||||
The community and the core team address GitHub issues. Be sure to check if a similar issue already exists. Please note that while we want to address everything immediately, we must prioritize.
|
||||
|
||||
### [Join our Discord](https://documen.so/discord)
|
||||
|
||||
You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
|
||||
|
||||
## Paid Account Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Private Discord channel
|
||||
|
||||
If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
|
||||
|
||||
## Enterprise Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Slack
|
||||
|
||||
If your team is on Slack, we can create a private workspace to support you more closely.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -64,4 +64,4 @@
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -81,4 +81,4 @@
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
apps/web/process-env.d.ts
vendored
2
apps/web/process-env.d.ts
vendored
@@ -16,5 +16,7 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET: string;
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
documentStatus={document.status}
|
||||
/>
|
||||
|
||||
<DownloadAuditLogButton documentId={document.id} />
|
||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,10 +9,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DownloadAuditLogButtonProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
||||
export const DownloadAuditLogButton = ({
|
||||
className,
|
||||
teamId,
|
||||
documentId,
|
||||
}: DownloadAuditLogButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isLoading } =
|
||||
@@ -20,7 +25,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadAuditLogs({ documentId });
|
||||
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
@@ -41,6 +42,7 @@ export const DeleteDocumentDialog = ({
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { refreshLimits } = useLimits();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
@@ -48,6 +50,7 @@ export const DeleteDocumentDialog = ({
|
||||
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document deleted',
|
||||
|
||||
@@ -36,7 +36,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { quota, remaining } = useLimits();
|
||||
const { quota, remaining, refreshLimits } = useLimits();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -71,6 +71,8 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document uploaded',
|
||||
description: 'Your document has been uploaded successfully.',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
@@ -39,6 +39,7 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateToken,
|
||||
}: TemplatesDirectPageViewProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -82,8 +83,15 @@ export const DirectTemplatePageView = ({
|
||||
|
||||
const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
|
||||
try {
|
||||
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
|
||||
|
||||
if (directTemplateExternalId) {
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
const token = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
directRecipientEmail: recipient.email,
|
||||
templateUpdatedAt: template.updatedAt,
|
||||
|
||||
@@ -41,6 +41,7 @@ import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
@@ -182,6 +183,15 @@ export const SignDirectTemplateForm = ({
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
key={field.id}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
||||
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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@@ -77,6 +78,9 @@ export default async function CompletedSigningPage({
|
||||
}
|
||||
|
||||
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
|
||||
const isExistingUser = await getUserByEmail({ email: recipient.email })
|
||||
.then((u) => !!u)
|
||||
.catch(() => false);
|
||||
|
||||
const recipientName =
|
||||
recipient.name ||
|
||||
@@ -85,7 +89,7 @@ export default async function CompletedSigningPage({
|
||||
|
||||
const sessionData = await getServerSession();
|
||||
const isLoggedIn = !!sessionData?.user;
|
||||
const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type InitialsFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const InitialsField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: InitialsFieldProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { fullName } = useRequiredSigningContext();
|
||||
const initials = extractInitials(fullName);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const {
|
||||
mutateAsync: removeSignedFieldWithToken,
|
||||
isLoading: isRemoveSignedFieldWithTokenLoading,
|
||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||
try {
|
||||
const value = initials ?? '';
|
||||
|
||||
const payload: TSignFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value,
|
||||
isBase64: false,
|
||||
authOptions,
|
||||
};
|
||||
|
||||
if (onSignField) {
|
||||
await onSignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await signFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while signing the document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = async () => {
|
||||
try {
|
||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
};
|
||||
|
||||
if (onUnsignField) {
|
||||
await onUnsignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeSignedFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while removing the signature.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials">
|
||||
{isLoading && (
|
||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!field.inserted && (
|
||||
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
|
||||
Initials
|
||||
</p>
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
||||
{field.customText}
|
||||
</p>
|
||||
)}
|
||||
</SigningFieldContainer>
|
||||
);
|
||||
};
|
||||
@@ -39,7 +39,16 @@ export type SignatureFieldProps = {
|
||||
*/
|
||||
onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise<void> | void;
|
||||
onRemove?: (fieldType?: string) => Promise<void> | void;
|
||||
type?: 'Date' | 'Email' | 'Name' | 'Signature' | 'Radio' | 'Dropdown' | 'Number' | 'Checkbox';
|
||||
type?:
|
||||
| 'Date'
|
||||
| 'Initials'
|
||||
| 'Email'
|
||||
| 'Name'
|
||||
| 'Signature'
|
||||
| 'Radio'
|
||||
| 'Dropdown'
|
||||
| 'Number'
|
||||
| 'Checkbox';
|
||||
tooltipText?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DateField } from './date-field';
|
||||
import { DropdownField } from './dropdown-field';
|
||||
import { EmailField } from './email-field';
|
||||
import { SigningForm } from './form';
|
||||
import { InitialsField } from './initials-field';
|
||||
import { NameField } from './name-field';
|
||||
import { NumberField } from './number-field';
|
||||
import { RadioField } from './radio-field';
|
||||
@@ -101,6 +102,9 @@ export const SigningPageView = ({
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
|
||||
@@ -4,7 +4,11 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import {
|
||||
IS_GOOGLE_SSO_ENABLED,
|
||||
IS_OIDC_SSO_ENABLED,
|
||||
OIDC_PROVIDER_LABEL,
|
||||
} from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
@@ -43,6 +47,7 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||
oidcProviderLabel={OIDC_PROVIDER_LABEL}
|
||||
/>
|
||||
|
||||
{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
|
||||
@@ -98,6 +98,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
||||
{
|
||||
type: P.union(
|
||||
FieldType.NAME,
|
||||
FieldType.INITIALS,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NUMBER,
|
||||
FieldType.RADIO,
|
||||
|
||||
@@ -71,6 +71,7 @@ export type SignInFormProps = {
|
||||
initialEmail?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
oidcProviderLabel?: string;
|
||||
};
|
||||
|
||||
export const SignInForm = ({
|
||||
@@ -78,6 +79,7 @@ export const SignInForm = ({
|
||||
initialEmail,
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
oidcProviderLabel,
|
||||
}: SignInFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const { getFlag } = useFeatureFlags();
|
||||
@@ -369,7 +371,7 @@ export const SignInForm = ({
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||
OIDC
|
||||
{oidcProviderLabel || 'OIDC'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -60,13 +60,23 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
},
|
||||
linkAccount: async ({ user }) => {
|
||||
linkAccount: async ({ user, account, profile }) => {
|
||||
const userId = typeof user.id === 'string' ? parseInt(user.id) : user.id;
|
||||
|
||||
if (isNaN(userId)) {
|
||||
if (Number.isNaN(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is linking an OIDC account and the email verified date is set then update it in the db.
|
||||
if (account.provider === 'oidc' && profile.emailVerified !== null) {
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
emailVerified: profile.emailVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"apps/marketing": {
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@@ -424,7 +424,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
@@ -80,4 +80,4 @@
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { createNextRoute } from '@ts-rest/next';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import '@documenso/lib/constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
@@ -222,6 +225,36 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormat = body.meta.dateFormat
|
||||
? DATE_FORMATS.find((format) => format.label === body.meta.dateFormat)
|
||||
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||
const timezone = body.meta.timezone
|
||||
? TIME_ZONES.find((tz) => tz === body.meta.timezone)
|
||||
: DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
const isDateFormatValid = body.meta.dateFormat
|
||||
? DATE_FORMATS.some((format) => format.label === dateFormat?.label)
|
||||
: true;
|
||||
const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
||||
|
||||
if (!isDateFormatValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid date format. Please provide a valid date format',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!isTimeZoneValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid timezone. Please provide a valid timezone',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
||||
|
||||
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
||||
@@ -244,7 +277,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
...body.meta,
|
||||
subject: body.meta.subject,
|
||||
message: body.meta.message,
|
||||
timezone,
|
||||
dateFormat: dateFormat?.value,
|
||||
redirectUrl: body.meta.redirectUrl,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import '@documenso/lib/constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
||||
import {
|
||||
DocumentDataType,
|
||||
@@ -11,6 +15,8 @@ import {
|
||||
TemplateType,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZNoBodyMutationSchema = null;
|
||||
|
||||
/**
|
||||
@@ -97,8 +103,19 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
timezone: z.string().default(DEFAULT_DOCUMENT_TIME_ZONE).openapi({
|
||||
description:
|
||||
'The timezone of the date. Must be one of the options listed in the list below.',
|
||||
enum: TIME_ZONES,
|
||||
}),
|
||||
dateFormat: z
|
||||
.string()
|
||||
.default(DEFAULT_DOCUMENT_DATE_FORMAT)
|
||||
.openapi({
|
||||
description:
|
||||
'The format of the date. Must be one of the options listed in the list below.',
|
||||
enum: DATE_FORMATS.map((format) => format.value),
|
||||
}),
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { equals } from 'remeda';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getLimits } from '../client';
|
||||
import { FREE_PLAN_LIMITS } from '../constants';
|
||||
import type { TLimitsResponseSchema } from '../schema';
|
||||
|
||||
export type LimitsContextValue = TLimitsResponseSchema;
|
||||
export type LimitsContextValue = TLimitsResponseSchema & { refreshLimits: () => Promise<void> };
|
||||
|
||||
const LimitsContext = createContext<LimitsContextValue | null>(null);
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useLimits = () => {
|
||||
};
|
||||
|
||||
export type LimitsProviderProps = {
|
||||
initialValue?: LimitsContextValue;
|
||||
initialValue?: TLimitsResponseSchema;
|
||||
teamId?: number;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export const LimitsProvider = ({
|
||||
}: LimitsProviderProps) => {
|
||||
const [limits, setLimits] = useState(() => initialValue);
|
||||
|
||||
const refreshLimits = async () => {
|
||||
const refreshLimits = useCallback(async () => {
|
||||
const newLimits = await getLimits({ teamId });
|
||||
|
||||
setLimits((oldLimits) => {
|
||||
@@ -48,11 +48,11 @@ export const LimitsProvider = ({
|
||||
|
||||
return newLimits;
|
||||
});
|
||||
};
|
||||
}, [teamId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshLimits();
|
||||
}, []);
|
||||
}, [refreshLimits]);
|
||||
|
||||
useEffect(() => {
|
||||
const onFocus = () => {
|
||||
@@ -64,7 +64,16 @@ export const LimitsProvider = ({
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus);
|
||||
};
|
||||
}, []);
|
||||
}, [refreshLimits]);
|
||||
|
||||
return <LimitsContext.Provider value={limits}>{children}</LimitsContext.Provider>;
|
||||
return (
|
||||
<LimitsContext.Provider
|
||||
value={{
|
||||
...limits,
|
||||
refreshLimits,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LimitsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { getLimits } from '../client';
|
||||
import type { LimitsContextValue } from './client';
|
||||
import { LimitsProvider as ClientLimitsProvider } from './client';
|
||||
|
||||
export type LimitsProviderProps = {
|
||||
@@ -14,7 +13,7 @@ export type LimitsProviderProps = {
|
||||
export const LimitsProvider = async ({ children, teamId }: LimitsProviderProps) => {
|
||||
const requestHeaders = Object.fromEntries(headers().entries());
|
||||
|
||||
const limits: LimitsContextValue = await getLimits({ headers: requestHeaders, teamId });
|
||||
const limits = await getLimits({ headers: requestHeaders, teamId });
|
||||
|
||||
return (
|
||||
<ClientLimitsProvider initialValue={limits} teamId={teamId}>
|
||||
|
||||
@@ -18,6 +18,8 @@ export const IS_OIDC_SSO_ENABLED = Boolean(
|
||||
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
|
||||
);
|
||||
|
||||
export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL;
|
||||
|
||||
export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: string } = {
|
||||
[UserSecurityAuditLogType.ACCOUNT_SSO_LINK]: 'Linked account to SSO',
|
||||
[UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE]: 'Profile updated',
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const URL_REGEX =
|
||||
/^(https?):\/\/(?:www\.)?(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
|
||||
@@ -161,7 +161,10 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
id: profile.sub,
|
||||
email: profile.email || profile.preferred_username,
|
||||
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
|
||||
emailVerified: profile.email_verified ? new Date().toISOString() : null,
|
||||
emailVerified:
|
||||
process.env.NEXT_PRIVATE_OIDC_SKIP_VERIFY === 'true' || profile.email_verified
|
||||
? new Date().toISOString()
|
||||
: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -361,6 +364,12 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
|
||||
async signIn({ user }) {
|
||||
// This statement appears above so we can stil allow `oidc` connections
|
||||
// while other signups are disabled.
|
||||
if (env('NEXT_PRIVATE_OIDC_ALLOW_SIGNUP') === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We do this to stop OAuth providers from creating an account
|
||||
// when signups are disabled
|
||||
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { URL_REGEX } from '../constants/url-regex';
|
||||
import { isValidRedirectUrl } from '../utils/is-valid-redirect-url';
|
||||
|
||||
/**
|
||||
* Note this allows empty strings.
|
||||
*/
|
||||
export const ZUrlSchema = z
|
||||
.string()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
});
|
||||
|
||||
@@ -231,10 +231,17 @@ export const signFieldWithToken = async ({
|
||||
type,
|
||||
data: signatureImageAsBase64 || typedSignature || '',
|
||||
}))
|
||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({
|
||||
type,
|
||||
data: updatedField.customText,
|
||||
}))
|
||||
.with(
|
||||
FieldType.DATE,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NAME,
|
||||
FieldType.TEXT,
|
||||
FieldType.INITIALS,
|
||||
(type) => ({
|
||||
type,
|
||||
data: updatedField.customText,
|
||||
}),
|
||||
)
|
||||
.with(
|
||||
FieldType.NUMBER,
|
||||
FieldType.RADIO,
|
||||
|
||||
@@ -133,9 +133,14 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
throw new Error('Invalid checkbox field meta');
|
||||
}
|
||||
|
||||
const values = meta.data.values?.map((item) => ({
|
||||
...item,
|
||||
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||
}));
|
||||
|
||||
const selected = field.customText.split(',');
|
||||
|
||||
for (const [index, item] of (meta.data.values ?? []).entries()) {
|
||||
for (const [index, item] of (values ?? []).entries()) {
|
||||
const offsetY = index * 16;
|
||||
|
||||
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
|
||||
@@ -169,9 +174,14 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
throw new Error('Invalid radio field meta');
|
||||
}
|
||||
|
||||
const values = meta?.data.values?.map((item) => ({
|
||||
...item,
|
||||
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||
}));
|
||||
|
||||
const selected = field.customText.split(',');
|
||||
|
||||
for (const [index, item] of (meta.data.values ?? []).entries()) {
|
||||
for (const [index, item] of (values ?? []).entries()) {
|
||||
const offsetY = index * 16;
|
||||
|
||||
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
||||
|
||||
@@ -44,6 +44,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
|
||||
directRecipientName?: string;
|
||||
directRecipientEmail: string;
|
||||
directTemplateToken: string;
|
||||
directTemplateExternalId?: string;
|
||||
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
||||
templateUpdatedAt: Date;
|
||||
requestMetadata: RequestMetadata;
|
||||
@@ -63,6 +64,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directRecipientName: initialDirectRecipientName,
|
||||
directRecipientEmail,
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
signedFieldValues,
|
||||
templateUpdatedAt,
|
||||
requestMetadata,
|
||||
@@ -227,6 +229,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
title: template.title,
|
||||
createdAt: initialRequestTime,
|
||||
status: DocumentStatus.PENDING,
|
||||
externalId: directTemplateExternalId,
|
||||
documentDataId: documentData.id,
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
@@ -465,6 +468,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
.with(
|
||||
FieldType.DATE,
|
||||
FieldType.EMAIL,
|
||||
FieldType.INITIALS,
|
||||
FieldType.NAME,
|
||||
FieldType.TEXT,
|
||||
FieldType.NUMBER,
|
||||
|
||||
@@ -233,6 +233,10 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
||||
|
||||
// Organised into union to allow us to extend each field if required.
|
||||
field: z.union([
|
||||
z.object({
|
||||
type: z.literal(FieldType.INITIALS),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.EMAIL),
|
||||
data: z.string(),
|
||||
|
||||
16
packages/lib/utils/is-valid-redirect-url.ts
Normal file
16
packages/lib/utils/is-valid-redirect-url.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const ALLOWED_PROTOCOLS = ['http', 'https'];
|
||||
|
||||
export const isValidRedirectUrl = (value: string) => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
|
||||
console.log({ protocol: url.protocol });
|
||||
if (!ALLOWED_PROTOCOLS.includes(url.protocol.slice(0, -1).toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,481 +0,0 @@
|
||||
import type { ColumnType } from 'kysely';
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const IdentityProvider = {
|
||||
DOCUMENSO: 'DOCUMENSO',
|
||||
GOOGLE: 'GOOGLE',
|
||||
OIDC: 'OIDC',
|
||||
} as const;
|
||||
export type IdentityProvider = (typeof IdentityProvider)[keyof typeof IdentityProvider];
|
||||
export const Role = {
|
||||
ADMIN: 'ADMIN',
|
||||
USER: 'USER',
|
||||
} as const;
|
||||
export type Role = (typeof Role)[keyof typeof Role];
|
||||
export const UserSecurityAuditLogType = {
|
||||
ACCOUNT_PROFILE_UPDATE: 'ACCOUNT_PROFILE_UPDATE',
|
||||
ACCOUNT_SSO_LINK: 'ACCOUNT_SSO_LINK',
|
||||
AUTH_2FA_DISABLE: 'AUTH_2FA_DISABLE',
|
||||
AUTH_2FA_ENABLE: 'AUTH_2FA_ENABLE',
|
||||
PASSKEY_CREATED: 'PASSKEY_CREATED',
|
||||
PASSKEY_DELETED: 'PASSKEY_DELETED',
|
||||
PASSKEY_UPDATED: 'PASSKEY_UPDATED',
|
||||
PASSWORD_RESET: 'PASSWORD_RESET',
|
||||
PASSWORD_UPDATE: 'PASSWORD_UPDATE',
|
||||
SIGN_OUT: 'SIGN_OUT',
|
||||
SIGN_IN: 'SIGN_IN',
|
||||
SIGN_IN_FAIL: 'SIGN_IN_FAIL',
|
||||
SIGN_IN_2FA_FAIL: 'SIGN_IN_2FA_FAIL',
|
||||
SIGN_IN_PASSKEY_FAIL: 'SIGN_IN_PASSKEY_FAIL',
|
||||
} as const;
|
||||
export type UserSecurityAuditLogType =
|
||||
(typeof UserSecurityAuditLogType)[keyof typeof UserSecurityAuditLogType];
|
||||
export const WebhookTriggerEvents = {
|
||||
DOCUMENT_CREATED: 'DOCUMENT_CREATED',
|
||||
DOCUMENT_SENT: 'DOCUMENT_SENT',
|
||||
DOCUMENT_OPENED: 'DOCUMENT_OPENED',
|
||||
DOCUMENT_SIGNED: 'DOCUMENT_SIGNED',
|
||||
DOCUMENT_COMPLETED: 'DOCUMENT_COMPLETED',
|
||||
} as const;
|
||||
export type WebhookTriggerEvents = (typeof WebhookTriggerEvents)[keyof typeof WebhookTriggerEvents];
|
||||
export const WebhookCallStatus = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAILED: 'FAILED',
|
||||
} as const;
|
||||
export type WebhookCallStatus = (typeof WebhookCallStatus)[keyof typeof WebhookCallStatus];
|
||||
export const ApiTokenAlgorithm = {
|
||||
SHA512: 'SHA512',
|
||||
} as const;
|
||||
export type ApiTokenAlgorithm = (typeof ApiTokenAlgorithm)[keyof typeof ApiTokenAlgorithm];
|
||||
export const SubscriptionStatus = {
|
||||
ACTIVE: 'ACTIVE',
|
||||
PAST_DUE: 'PAST_DUE',
|
||||
INACTIVE: 'INACTIVE',
|
||||
} as const;
|
||||
export type SubscriptionStatus = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
|
||||
export const DocumentStatus = {
|
||||
DRAFT: 'DRAFT',
|
||||
PENDING: 'PENDING',
|
||||
COMPLETED: 'COMPLETED',
|
||||
} as const;
|
||||
export type DocumentStatus = (typeof DocumentStatus)[keyof typeof DocumentStatus];
|
||||
export const DocumentSource = {
|
||||
DOCUMENT: 'DOCUMENT',
|
||||
TEMPLATE: 'TEMPLATE',
|
||||
TEMPLATE_DIRECT_LINK: 'TEMPLATE_DIRECT_LINK',
|
||||
} as const;
|
||||
export type DocumentSource = (typeof DocumentSource)[keyof typeof DocumentSource];
|
||||
export const DocumentDataType = {
|
||||
S3_PATH: 'S3_PATH',
|
||||
BYTES: 'BYTES',
|
||||
BYTES_64: 'BYTES_64',
|
||||
} as const;
|
||||
export type DocumentDataType = (typeof DocumentDataType)[keyof typeof DocumentDataType];
|
||||
export const ReadStatus = {
|
||||
NOT_OPENED: 'NOT_OPENED',
|
||||
OPENED: 'OPENED',
|
||||
} as const;
|
||||
export type ReadStatus = (typeof ReadStatus)[keyof typeof ReadStatus];
|
||||
export const SendStatus = {
|
||||
NOT_SENT: 'NOT_SENT',
|
||||
SENT: 'SENT',
|
||||
} as const;
|
||||
export type SendStatus = (typeof SendStatus)[keyof typeof SendStatus];
|
||||
export const SigningStatus = {
|
||||
NOT_SIGNED: 'NOT_SIGNED',
|
||||
SIGNED: 'SIGNED',
|
||||
} as const;
|
||||
export type SigningStatus = (typeof SigningStatus)[keyof typeof SigningStatus];
|
||||
export const RecipientRole = {
|
||||
CC: 'CC',
|
||||
SIGNER: 'SIGNER',
|
||||
VIEWER: 'VIEWER',
|
||||
APPROVER: 'APPROVER',
|
||||
} as const;
|
||||
export type RecipientRole = (typeof RecipientRole)[keyof typeof RecipientRole];
|
||||
export const FieldType = {
|
||||
SIGNATURE: 'SIGNATURE',
|
||||
FREE_SIGNATURE: 'FREE_SIGNATURE',
|
||||
NAME: 'NAME',
|
||||
EMAIL: 'EMAIL',
|
||||
DATE: 'DATE',
|
||||
TEXT: 'TEXT',
|
||||
NUMBER: 'NUMBER',
|
||||
RADIO: 'RADIO',
|
||||
CHECKBOX: 'CHECKBOX',
|
||||
DROPDOWN: 'DROPDOWN',
|
||||
} as const;
|
||||
export type FieldType = (typeof FieldType)[keyof typeof FieldType];
|
||||
export const TeamMemberRole = {
|
||||
ADMIN: 'ADMIN',
|
||||
MANAGER: 'MANAGER',
|
||||
MEMBER: 'MEMBER',
|
||||
} as const;
|
||||
export type TeamMemberRole = (typeof TeamMemberRole)[keyof typeof TeamMemberRole];
|
||||
export const TeamMemberInviteStatus = {
|
||||
ACCEPTED: 'ACCEPTED',
|
||||
PENDING: 'PENDING',
|
||||
} as const;
|
||||
export type TeamMemberInviteStatus =
|
||||
(typeof TeamMemberInviteStatus)[keyof typeof TeamMemberInviteStatus];
|
||||
export const TemplateType = {
|
||||
PUBLIC: 'PUBLIC',
|
||||
PRIVATE: 'PRIVATE',
|
||||
} as const;
|
||||
export type TemplateType = (typeof TemplateType)[keyof typeof TemplateType];
|
||||
export type Account = {
|
||||
id: string;
|
||||
userId: number;
|
||||
type: string;
|
||||
provider: string;
|
||||
providerAccountId: string;
|
||||
refresh_token: string | null;
|
||||
access_token: string | null;
|
||||
expires_at: number | null;
|
||||
created_at: number | null;
|
||||
ext_expires_in: number | null;
|
||||
token_type: string | null;
|
||||
scope: string | null;
|
||||
id_token: string | null;
|
||||
session_state: string | null;
|
||||
};
|
||||
export type AnonymousVerificationToken = {
|
||||
id: string;
|
||||
token: string;
|
||||
expiresAt: Timestamp;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type ApiToken = {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
token: string;
|
||||
algorithm: Generated<ApiTokenAlgorithm>;
|
||||
expires: Timestamp | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
userId: number | null;
|
||||
teamId: number | null;
|
||||
};
|
||||
export type Document = {
|
||||
id: Generated<number>;
|
||||
userId: number;
|
||||
authOptions: unknown | null;
|
||||
formValues: unknown | null;
|
||||
title: string;
|
||||
status: Generated<DocumentStatus>;
|
||||
documentDataId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
completedAt: Timestamp | null;
|
||||
deletedAt: Timestamp | null;
|
||||
teamId: number | null;
|
||||
templateId: number | null;
|
||||
source: DocumentSource;
|
||||
};
|
||||
export type DocumentAuditLog = {
|
||||
id: string;
|
||||
documentId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
type: string;
|
||||
data: unknown;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
userId: number | null;
|
||||
userAgent: string | null;
|
||||
ipAddress: string | null;
|
||||
};
|
||||
export type DocumentData = {
|
||||
id: string;
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
initialData: string;
|
||||
};
|
||||
export type DocumentMeta = {
|
||||
id: string;
|
||||
subject: string | null;
|
||||
message: string | null;
|
||||
timezone: Generated<string | null>;
|
||||
password: string | null;
|
||||
dateFormat: Generated<string | null>;
|
||||
documentId: number;
|
||||
redirectUrl: string | null;
|
||||
};
|
||||
export type DocumentShareLink = {
|
||||
id: Generated<number>;
|
||||
email: string;
|
||||
slug: string;
|
||||
documentId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Timestamp;
|
||||
};
|
||||
export type Field = {
|
||||
id: Generated<number>;
|
||||
secondaryId: string;
|
||||
documentId: number | null;
|
||||
templateId: number | null;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
page: number;
|
||||
positionX: Generated<string>;
|
||||
positionY: Generated<string>;
|
||||
width: Generated<string>;
|
||||
height: Generated<string>;
|
||||
customText: string;
|
||||
inserted: boolean;
|
||||
fieldMeta: unknown | null;
|
||||
};
|
||||
export type Passkey = {
|
||||
id: string;
|
||||
userId: number;
|
||||
name: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
lastUsedAt: Timestamp | null;
|
||||
credentialId: Buffer;
|
||||
credentialPublicKey: Buffer;
|
||||
counter: string;
|
||||
credentialDeviceType: string;
|
||||
credentialBackedUp: boolean;
|
||||
transports: string[];
|
||||
};
|
||||
export type PasswordResetToken = {
|
||||
id: Generated<number>;
|
||||
token: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
expiry: Timestamp;
|
||||
userId: number;
|
||||
};
|
||||
export type Recipient = {
|
||||
id: Generated<number>;
|
||||
documentId: number | null;
|
||||
templateId: number | null;
|
||||
email: string;
|
||||
name: Generated<string>;
|
||||
token: string;
|
||||
documentDeletedAt: Timestamp | null;
|
||||
expired: Timestamp | null;
|
||||
signedAt: Timestamp | null;
|
||||
authOptions: unknown | null;
|
||||
role: Generated<RecipientRole>;
|
||||
readStatus: Generated<ReadStatus>;
|
||||
signingStatus: Generated<SigningStatus>;
|
||||
sendStatus: Generated<SendStatus>;
|
||||
};
|
||||
export type Session = {
|
||||
id: string;
|
||||
sessionToken: string;
|
||||
userId: number;
|
||||
expires: Timestamp;
|
||||
};
|
||||
export type Signature = {
|
||||
id: Generated<number>;
|
||||
created: Generated<Timestamp>;
|
||||
recipientId: number;
|
||||
fieldId: number;
|
||||
signatureImageAsBase64: string | null;
|
||||
typedSignature: string | null;
|
||||
};
|
||||
export type SiteSettings = {
|
||||
id: string;
|
||||
enabled: Generated<boolean>;
|
||||
data: unknown;
|
||||
lastModifiedByUserId: number | null;
|
||||
lastModifiedAt: Generated<Timestamp>;
|
||||
};
|
||||
export type Subscription = {
|
||||
id: Generated<number>;
|
||||
status: Generated<SubscriptionStatus>;
|
||||
planId: string;
|
||||
priceId: string;
|
||||
periodEnd: Timestamp | null;
|
||||
userId: number | null;
|
||||
teamId: number | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Timestamp;
|
||||
cancelAtPeriodEnd: Generated<boolean>;
|
||||
};
|
||||
export type Team = {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
url: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
customerId: string | null;
|
||||
ownerUserId: number;
|
||||
};
|
||||
export type TeamEmail = {
|
||||
teamId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
export type TeamEmailVerification = {
|
||||
teamId: number;
|
||||
name: string;
|
||||
email: string;
|
||||
token: string;
|
||||
expiresAt: Timestamp;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type TeamMember = {
|
||||
id: Generated<number>;
|
||||
teamId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
role: TeamMemberRole;
|
||||
userId: number;
|
||||
};
|
||||
export type TeamMemberInvite = {
|
||||
id: Generated<number>;
|
||||
teamId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
email: string;
|
||||
status: Generated<TeamMemberInviteStatus>;
|
||||
role: TeamMemberRole;
|
||||
token: string;
|
||||
};
|
||||
export type TeamPending = {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
url: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
customerId: string;
|
||||
ownerUserId: number;
|
||||
};
|
||||
export type TeamTransferVerification = {
|
||||
teamId: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
email: string;
|
||||
token: string;
|
||||
expiresAt: Timestamp;
|
||||
createdAt: Generated<Timestamp>;
|
||||
clearPaymentMethods: Generated<boolean>;
|
||||
};
|
||||
export type Template = {
|
||||
id: Generated<number>;
|
||||
type: Generated<TemplateType>;
|
||||
title: string;
|
||||
userId: number;
|
||||
teamId: number | null;
|
||||
authOptions: unknown | null;
|
||||
templateDocumentDataId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
};
|
||||
export type TemplateDirectLink = {
|
||||
id: string;
|
||||
templateId: number;
|
||||
token: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
enabled: boolean;
|
||||
directTemplateRecipientId: number;
|
||||
};
|
||||
export type TemplateMeta = {
|
||||
id: string;
|
||||
subject: string | null;
|
||||
message: string | null;
|
||||
timezone: Generated<string | null>;
|
||||
password: string | null;
|
||||
dateFormat: Generated<string | null>;
|
||||
templateId: number;
|
||||
redirectUrl: string | null;
|
||||
};
|
||||
export type User = {
|
||||
id: Generated<number>;
|
||||
name: string | null;
|
||||
customerId: string | null;
|
||||
email: string;
|
||||
emailVerified: Timestamp | null;
|
||||
password: string | null;
|
||||
source: string | null;
|
||||
signature: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
lastSignedIn: Generated<Timestamp>;
|
||||
roles: Generated<Role[]>;
|
||||
identityProvider: Generated<IdentityProvider>;
|
||||
twoFactorSecret: string | null;
|
||||
twoFactorEnabled: Generated<boolean>;
|
||||
twoFactorBackupCodes: string | null;
|
||||
url: string | null;
|
||||
};
|
||||
export type UserProfile = {
|
||||
id: number;
|
||||
bio: string | null;
|
||||
};
|
||||
export type UserSecurityAuditLog = {
|
||||
id: Generated<number>;
|
||||
userId: number;
|
||||
createdAt: Generated<Timestamp>;
|
||||
type: UserSecurityAuditLogType;
|
||||
userAgent: string | null;
|
||||
ipAddress: string | null;
|
||||
};
|
||||
export type VerificationToken = {
|
||||
id: Generated<number>;
|
||||
secondaryId: string;
|
||||
identifier: string;
|
||||
token: string;
|
||||
expires: Timestamp;
|
||||
createdAt: Generated<Timestamp>;
|
||||
userId: number;
|
||||
};
|
||||
export type Webhook = {
|
||||
id: string;
|
||||
webhookUrl: string;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
secret: string | null;
|
||||
enabled: Generated<boolean>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
userId: number;
|
||||
teamId: number | null;
|
||||
};
|
||||
export type WebhookCall = {
|
||||
id: string;
|
||||
status: WebhookCallStatus;
|
||||
url: string;
|
||||
event: WebhookTriggerEvents;
|
||||
requestBody: unknown;
|
||||
responseCode: number;
|
||||
responseHeaders: unknown | null;
|
||||
responseBody: unknown | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
webhookId: string;
|
||||
};
|
||||
export type DB = {
|
||||
Account: Account;
|
||||
AnonymousVerificationToken: AnonymousVerificationToken;
|
||||
ApiToken: ApiToken;
|
||||
Document: Document;
|
||||
DocumentAuditLog: DocumentAuditLog;
|
||||
DocumentData: DocumentData;
|
||||
DocumentMeta: DocumentMeta;
|
||||
DocumentShareLink: DocumentShareLink;
|
||||
Field: Field;
|
||||
Passkey: Passkey;
|
||||
PasswordResetToken: PasswordResetToken;
|
||||
Recipient: Recipient;
|
||||
Session: Session;
|
||||
Signature: Signature;
|
||||
SiteSettings: SiteSettings;
|
||||
Subscription: Subscription;
|
||||
Team: Team;
|
||||
TeamEmail: TeamEmail;
|
||||
TeamEmailVerification: TeamEmailVerification;
|
||||
TeamMember: TeamMember;
|
||||
TeamMemberInvite: TeamMemberInvite;
|
||||
TeamPending: TeamPending;
|
||||
TeamTransferVerification: TeamTransferVerification;
|
||||
Template: Template;
|
||||
TemplateDirectLink: TemplateDirectLink;
|
||||
TemplateMeta: TemplateMeta;
|
||||
User: User;
|
||||
UserProfile: UserProfile;
|
||||
UserSecurityAuditLog: UserSecurityAuditLog;
|
||||
VerificationToken: VerificationToken;
|
||||
Webhook: Webhook;
|
||||
WebhookCall: WebhookCall;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "FieldType" ADD VALUE 'INITIALS';
|
||||
@@ -409,6 +409,7 @@ model Recipient {
|
||||
enum FieldType {
|
||||
SIGNATURE
|
||||
FREE_SIGNATURE
|
||||
INITIALS
|
||||
NAME
|
||||
EMAIL
|
||||
DATE
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ZCreateDocumentMutationSchema,
|
||||
ZDeleteDraftDocumentMutationSchema as ZDeleteDocumentMutationSchema,
|
||||
ZDownloadAuditLogsMutationSchema,
|
||||
ZDownloadCertificateMutationSchema,
|
||||
ZFindDocumentAuditLogsQuerySchema,
|
||||
ZGetDocumentByIdQuerySchema,
|
||||
ZGetDocumentByTokenQuerySchema,
|
||||
@@ -411,7 +412,14 @@ export const documentRouter = router({
|
||||
id: documentId,
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
}).catch(() => null);
|
||||
|
||||
if (!document || (teamId && document.teamId !== teamId)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You do not have access to this document.',
|
||||
});
|
||||
}
|
||||
|
||||
const encrypted = encryptSecondaryData({
|
||||
data: document.id.toString(),
|
||||
@@ -433,7 +441,7 @@ export const documentRouter = router({
|
||||
}),
|
||||
|
||||
downloadCertificate: authenticatedProcedure
|
||||
.input(ZDownloadAuditLogsMutationSchema)
|
||||
.input(ZDownloadCertificateMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { documentId, teamId } = input;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
@@ -65,8 +65,9 @@ export const ZSetSettingsForDocumentMutationSchema = z.object({
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message:
|
||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -131,8 +132,9 @@ export const ZSendDocumentMutationSchema = z.object({
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message:
|
||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -170,6 +172,11 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZDownloadCertificateMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZMoveDocumentsToTeamSchema = z.object({
|
||||
documentId: z.number(),
|
||||
teamId: z.number(),
|
||||
|
||||
@@ -66,6 +66,7 @@ export const templateRouter = router({
|
||||
directRecipientName,
|
||||
directRecipientEmail,
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
signedFieldValues,
|
||||
templateUpdatedAt,
|
||||
} = input;
|
||||
@@ -76,6 +77,7 @@ export const templateRouter = router({
|
||||
directRecipientName,
|
||||
directRecipientEmail,
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
signedFieldValues,
|
||||
templateUpdatedAt,
|
||||
user: ctx.user
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { TemplateType } from '@documenso/prisma/client';
|
||||
|
||||
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
||||
@@ -20,6 +20,7 @@ export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
|
||||
directRecipientName: z.string().optional(),
|
||||
directRecipientEmail: z.string().email(),
|
||||
directTemplateToken: z.string().min(1),
|
||||
directTemplateExternalId: z.string().optional(),
|
||||
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
|
||||
templateUpdatedAt: z.date(),
|
||||
});
|
||||
@@ -96,8 +97,9 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message:
|
||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
3
packages/tsconfig/process-env.d.ts
vendored
3
packages/tsconfig/process-env.d.ts
vendored
@@ -9,6 +9,9 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN?: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID?: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET?: string;
|
||||
NEXT_PRIVATE_OIDC_PROVIDER_LABEL?: string;
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
|
||||
|
||||
NEXT_PRIVATE_DATABASE_URL: string;
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
Contact,
|
||||
Disc,
|
||||
Hash,
|
||||
Info,
|
||||
@@ -457,10 +458,11 @@ export const AddFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
|
||||
selectedSignerStyles.default.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50': !isFieldWithinBounds,
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
@@ -649,6 +651,32 @@ export const AddFieldsFormPartial = ({
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
onClick={() => setSelectedField(FieldType.INITIALS)}
|
||||
onMouseDown={() => setSelectedField(FieldType.INITIALS)}
|
||||
data-selected={selectedField === FieldType.INITIALS ? true : undefined}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
Initials
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
@@ -662,7 +690,7 @@ export const AddFieldsFormPartial = ({
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
|
||||
@@ -82,8 +82,12 @@ export const AddSettingsFormPartial = ({
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
meta: {
|
||||
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
timezone:
|
||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||
DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat:
|
||||
DATE_FORMATS.find((format) => format.label === document.documentMeta?.dateFormat)
|
||||
?.value ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
redirectUrl: document.documentMeta?.redirectUrl ?? '',
|
||||
},
|
||||
},
|
||||
@@ -98,10 +102,20 @@ export const AddSettingsFormPartial = ({
|
||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||
// when the document is signed.
|
||||
useEffect(() => {
|
||||
if (!form.formState.touchedFields.meta?.timezone && !documentHasBeenSent) {
|
||||
if (
|
||||
!form.formState.touchedFields.meta?.timezone &&
|
||||
!documentHasBeenSent &&
|
||||
!document.documentMeta?.timezone
|
||||
) {
|
||||
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}
|
||||
}, [documentHasBeenSent, form, form.setValue, form.formState.touchedFields.meta?.timezone]);
|
||||
}, [
|
||||
documentHasBeenSent,
|
||||
form,
|
||||
form.setValue,
|
||||
form.formState.touchedFields.meta?.timezone,
|
||||
document.documentMeta?.timezone,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
@@ -34,8 +34,9 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message:
|
||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { CalendarDays, CheckSquare, ChevronDown, Disc, Hash, Mail, Type, User } from 'lucide-react';
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
Contact,
|
||||
Disc,
|
||||
Hash,
|
||||
Mail,
|
||||
Type,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
@@ -13,6 +23,7 @@ type FieldIconProps = {
|
||||
};
|
||||
|
||||
const fieldIcons = {
|
||||
[FieldType.INITIALS]: { icon: Contact, label: 'Initials' },
|
||||
[FieldType.EMAIL]: { icon: Mail, label: 'Email' },
|
||||
[FieldType.NAME]: { icon: User, label: 'Name' },
|
||||
[FieldType.DATE]: { icon: CalendarDays, label: 'Date' },
|
||||
@@ -46,9 +57,11 @@ export const FieldIcon = ({
|
||||
|
||||
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
|
||||
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
|
||||
label = fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
|
||||
label =
|
||||
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
|
||||
} else if (fieldMeta.label) {
|
||||
label = fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
|
||||
label =
|
||||
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
@@ -58,7 +71,7 @@ export const FieldIcon = ({
|
||||
|
||||
return (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-sm">
|
||||
<Icon className='h-4 w-4' /> {label}
|
||||
<Icon className="h-4 w-4" /> {label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export type TDocumentFlowFormSchema = z.infer<typeof ZDocumentFlowFormSchema>;
|
||||
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
||||
[FieldType.SIGNATURE]: 'Signature',
|
||||
[FieldType.FREE_SIGNATURE]: 'Free Signature',
|
||||
[FieldType.INITIALS]: 'Initials',
|
||||
[FieldType.TEXT]: 'Text',
|
||||
[FieldType.DATE]: 'Date',
|
||||
[FieldType.EMAIL]: 'Email',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
Contact,
|
||||
Disc,
|
||||
Hash,
|
||||
Mail,
|
||||
@@ -383,10 +384,11 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
|
||||
selectedSignerStyles.default.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50': !isFieldWithinBounds,
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
@@ -546,6 +548,32 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
onClick={() => setSelectedField(FieldType.INITIALS)}
|
||||
onMouseDown={() => setSelectedField(FieldType.INITIALS)}
|
||||
data-selected={selectedField === FieldType.INITIALS ? true : undefined}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
Initials
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
@@ -27,8 +27,9 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
|
||||
message:
|
||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
62
render.yaml
62
render.yaml
@@ -1,11 +1,11 @@
|
||||
services:
|
||||
- type: web
|
||||
runtime: node
|
||||
name: documenso-app
|
||||
env: node
|
||||
plan: free
|
||||
buildCommand: npm i && npm run build:web
|
||||
startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npm run start
|
||||
healthCheckPath: /api/trpc/health
|
||||
startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npx turbo run start --filter=@documenso/web
|
||||
healthCheckPath: /api/health
|
||||
|
||||
envVars:
|
||||
# Node Version
|
||||
@@ -98,6 +98,62 @@ services:
|
||||
- key: NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY
|
||||
sync: false
|
||||
|
||||
# Crypto
|
||||
- key: NEXT_PRIVATE_ENCRYPTION_KEY
|
||||
generateValue: true
|
||||
- key: NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
|
||||
generateValue: true
|
||||
|
||||
# Auth Optional
|
||||
- key: NEXT_PRIVATE_GOOGLE_CLIENT_ID
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_GOOGLE_CLIENT_SECRET
|
||||
sync: false
|
||||
|
||||
# Signing
|
||||
- key: NEXT_PRIVATE_SIGNING_TRANSPORT
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_PASSPHRASE
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS
|
||||
sync: false
|
||||
|
||||
# SMTP Optional
|
||||
- key: NEXT_PRIVATE_SMTP_APIKEY_USER
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SMTP_APIKEY
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_SMTP_SECURE
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_RESEND_API_KEY
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_MAILCHANNELS_API_KEY
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_MAILCHANNELS_ENDPOINT
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR
|
||||
sync: false
|
||||
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY
|
||||
sync: false
|
||||
- key: NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT
|
||||
sync: false
|
||||
|
||||
# Features Optional
|
||||
- key: NEXT_PUBLIC_DISABLE_SIGNUP
|
||||
sync: false
|
||||
|
||||
databases:
|
||||
- name: documenso-db
|
||||
plan: free
|
||||
|
||||
29
turbo.json
29
turbo.json
@@ -2,20 +2,12 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"prebuild",
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
".next/**",
|
||||
"!.next/cache/**"
|
||||
]
|
||||
"dependsOn": ["prebuild", "^build"],
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"prebuild": {
|
||||
"cache": false,
|
||||
"dependsOn": [
|
||||
"^prebuild"
|
||||
]
|
||||
"dependsOn": ["^prebuild"]
|
||||
},
|
||||
"lint": {
|
||||
"cache": false
|
||||
@@ -31,9 +23,7 @@
|
||||
"persistent": true
|
||||
},
|
||||
"start": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
@@ -41,18 +31,14 @@
|
||||
"cache": false
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
},
|
||||
"translate:compile": {
|
||||
"cache": false
|
||||
}
|
||||
},
|
||||
"globalDependencies": [
|
||||
"**/.env.*local"
|
||||
],
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"globalEnv": [
|
||||
"APP_VERSION",
|
||||
"NEXT_PRIVATE_ENCRYPTION_KEY",
|
||||
@@ -83,6 +69,9 @@
|
||||
"NEXT_PRIVATE_OIDC_WELL_KNOWN",
|
||||
"NEXT_PRIVATE_OIDC_CLIENT_ID",
|
||||
"NEXT_PRIVATE_OIDC_CLIENT_SECRET",
|
||||
"NEXT_PRIVATE_OIDC_PROVIDER_LABEL",
|
||||
"NEXT_PRIVATE_OIDC_ALLOW_SIGNUP",
|
||||
"NEXT_PRIVATE_OIDC_SKIP_VERIFY",
|
||||
"NEXT_PUBLIC_UPLOAD_TRANSPORT",
|
||||
"NEXT_PRIVATE_UPLOAD_ENDPOINT",
|
||||
"NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE",
|
||||
|
||||
Reference in New Issue
Block a user