diff --git a/apps/remix/app/components/forms/profile.tsx b/apps/remix/app/components/forms/profile.tsx index dec535fe0..4c5f7a06c 100644 --- a/apps/remix/app/components/forms/profile.tsx +++ b/apps/remix/app/components/forms/profile.tsx @@ -6,6 +6,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import { ZNameSchema } from '@documenso/lib/constants/auth'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -23,10 +24,7 @@ import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signa import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZProfileFormSchema = z.object({ - name: z - .string() - .trim() - .min(1, { message: msg`Please enter a valid name.`.id }), + name: ZNameSchema, signature: z.string().min(1, { message: msg`Signature Pad cannot be empty.`.id }), }); diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx index 7b767c3db..7fb718790 100644 --- a/apps/remix/app/components/forms/signup.tsx +++ b/apps/remix/app/components/forms/signup.tsx @@ -16,6 +16,7 @@ import { z } from 'zod'; import communityCardsImage from '@documenso/assets/images/community-cards.png'; import { authClient } from '@documenso/auth/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; +import { ZNameSchema } from '@documenso/lib/constants/auth'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { env } from '@documenso/lib/utils/env'; import { zEmail } from '@documenso/lib/utils/zod'; @@ -39,10 +40,7 @@ import { UserProfileTimur } from '~/components/general/user-profile-timur'; export const ZSignUpFormSchema = z .object({ - name: z - .string() - .trim() - .min(1, { message: msg`Please enter a valid name.`.id }), + name: ZNameSchema, email: zEmail().min(1), password: ZPasswordSchema, signature: z.string().min(1, { message: msg`We need your signature to sign documents`.id }), @@ -60,7 +58,7 @@ export const ZSignUpFormSchema = z export const SIGNUP_ERROR_MESSAGES: Record = { SIGNUP_DISABLED: msg`Signup is currently disabled or not available for your email domain.`, - [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`, + [AppErrorCode.ALREADY_EXISTS]: msg`We were unable to create your account. If you already have an account, try signing in instead.`, [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`, }; diff --git a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx index 9e2b1e4aa..ad7359c3f 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx @@ -25,12 +25,17 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender envelopeStatus, currentEnvelopeItem, fields, + signatures, recipients, getRecipientColorKey, setRenderError, overrideSettings, } = useCurrentEnvelopeRender(); + const signaturesByFieldId = useMemo(() => { + return new Map(signatures.map((signature) => [signature.fieldId, signature])); + }, [signatures]); + const { stage, pageLayer, konvaContainer, unscaledViewport } = usePageRenderer( ({ stage, pageLayer }) => { createPageCanvas(stage, pageLayer); @@ -80,6 +85,16 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender const fieldTranslations = getClientSideFieldTranslations(i18n); + // Look up an inserted signature for this field. If we don't have one (e.g. + // the signatures haven't been loaded, or the field hasn't been signed yet) + // fall back to a placeholder so the field still renders something. + const insertedSignature = signaturesByFieldId.get(field.id); + + const signature = insertedSignature ?? { + signatureImageAsBase64: '', + typedSignature: fieldTranslations.SIGNATURE, + }; + renderField({ scale, pageLayer: pageLayer.current, @@ -91,10 +106,7 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender positionX: Number(field.positionX), positionY: Number(field.positionY), fieldMeta: field.fieldMeta, - signature: { - signatureImageAsBase64: '', - typedSignature: fieldTranslations.SIGNATURE, - }, + signature, }, translations: fieldTranslations, pageWidth: unscaledViewport.width, @@ -150,7 +162,7 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender }); pageLayer.current.batchDraw(); - }, [localPageFields]); + }, [localPageFields, signaturesByFieldId]); if (!currentEnvelopeItem) { return null; diff --git a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx index beaf80a2d..05b6a08fe 100644 --- a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx +++ b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx @@ -1,14 +1,10 @@ import { useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import { - DocumentStatus, - EnvelopeType, - type Recipient, - type TemplateDirectLink, -} from '@prisma/client'; +import { DocumentStatus, EnvelopeType, type TemplateDirectLink } from '@prisma/client'; import { Copy, + Download, Edit, FolderIcon, MoreHorizontal, @@ -30,6 +26,7 @@ import { } from '@documenso/ui/primitives/dropdown-menu'; import { EnvelopeDeleteDialog } from '../dialogs/envelope-delete-dialog'; +import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog'; import { EnvelopeDuplicateDialog } from '../dialogs/envelope-duplicate-dialog'; import { EnvelopeRenameDialog } from '../dialogs/envelope-rename-dialog'; import { TemplateBulkSendDialog } from '../dialogs/template-bulk-send-dialog'; @@ -77,87 +74,94 @@ export const TemplatesTableActionDropdown = ({ Action - - - - Edit - - - - {canMutate && ( - setRenameDialogOpen(true)}> - - Rename - - )} - - {canMutate && ( - e.preventDefault()}> -
- - Duplicate -
- - } - /> - )} - - {canMutate && ( - - - Direct link + e.preventDefault()}> +
+ + Download
- } - /> - )} - - setMoveToFolderDialogOpen(true)}> - - Move to Folder - + + } + /> {canMutate && ( - - - Bulk Send via CSV - - } - /> - )} + <> + + + + Edit + + - {canMutate && ( - e.preventDefault()}> -
- - Delete + setRenameDialogOpen(true)}> + + Rename + + + e.preventDefault()}> +
+ + Duplicate +
+ + } + /> + + + + Direct link
- - } - /> + } + /> + + setMoveToFolderDialogOpen(true)}> + + Move to Folder + + + + + Bulk Send via CSV + + } + /> + + e.preventDefault()}> +
+ + Delete +
+ + } + /> + )}
diff --git a/apps/remix/app/entry.server.tsx b/apps/remix/app/entry.server.tsx index 6caeba746..d38ef3dea 100644 --- a/apps/remix/app/entry.server.tsx +++ b/apps/remix/app/entry.server.tsx @@ -20,7 +20,7 @@ export default async function handleRequest( responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, - _loadContext: AppLoadContext, + loadContext: AppLoadContext, ) { let language = await langCookie.parse(request.headers.get('cookie') ?? ''); @@ -30,6 +30,12 @@ export default async function handleRequest( await dynamicActivate(language); + // Threaded into ServerRouter so React Router applies the nonce to the + // scripts it injects (route manifest, hydration data, module preloads). + // The same nonce is also exposed to the React tree via the root loader so + // our own inline scripts/styles can carry it. + const nonce = loadContext.nonce || undefined; + return new Promise((resolve, reject) => { let shellRendered = false; const userAgent = request.headers.get('user-agent'); @@ -41,9 +47,10 @@ export default async function handleRequest( const { pipe, abort } = renderToPipeableStream( - + , { + nonce, [readyOption]() { shellRendered = true; const body = new PassThrough(); diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 47ebf0262..9fbcecb86 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -27,6 +27,7 @@ import { GenericErrorLayout } from './components/general/generic-error-layout'; import { langCookie } from './storage/lang-cookie.server'; import { themeSessionResolver } from './storage/theme-session.server'; import { appMetaTags } from './utils/meta'; +import { nonce } from './utils/nonce'; export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]; @@ -41,7 +42,7 @@ export function meta() { */ export const shouldRevalidate = () => false; -export async function loader({ request }: Route.LoaderArgs) { +export async function loader({ context, request }: Route.LoaderArgs) { const session = await getOptionalSession(request); const { getTheme } = await themeSessionResolver(request); @@ -67,6 +68,10 @@ export async function loader({ request }: Route.LoaderArgs) { lang, theme: getTheme(), disableAnimations, + // Surface the per-request CSP nonce produced by `securityHeadersMiddleware` so all + // SSR-rendered + {/* Global license banner currently disabled. Need to wait until after a few releases. */} @@ -152,13 +164,14 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {