Compare commits
14 Commits
chore/add-
...
v1.6.1-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.0",
|
||||
"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.0",
|
||||
"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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"apps/marketing": {
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.0",
|
||||
"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.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.0",
|
||||
"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.',
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -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 || 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;
|
||||
|
||||
@@ -457,10 +457,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={{
|
||||
|
||||
@@ -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.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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