mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
Compare commits
2 Commits
chore/dece
...
wip/rr7
| Author | SHA1 | Date | |
|---|---|---|---|
| 84ad7a6c2b | |||
| 866b036484 |
@ -16,7 +16,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"nextra": "^2.13.4",
|
"nextra": "^2.13.4",
|
||||||
"nextra-theme-docs": "^2.13.4",
|
"nextra-theme-docs": "^2.13.4",
|
||||||
@ -27,6 +27,6 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"typescript": "5.2.2"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13,11 +13,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"next": "14.2.23"
|
"next": "14.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "20.16.5",
|
||||||
"@types/react": "^18",
|
"@types/react": "18.3.5",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
apps/remix/.dockerignore
Normal file
4
apps/remix/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
9
apps/remix/.gitignore
vendored
Normal file
9
apps/remix/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.*.timestamp*
|
||||||
22
apps/remix/Dockerfile
Normal file
22
apps/remix/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
25
apps/remix/Dockerfile.bun
Normal file
25
apps/remix/Dockerfile.bun
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM oven/bun:1 AS dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --production
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
26
apps/remix/Dockerfile.pnpm
Normal file
26
apps/remix/Dockerfile.pnpm
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM node:20-alpine AS dependencies-env
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --prod --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
100
apps/remix/README.md
Normal file
100
apps/remix/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
This template includes three Dockerfiles optimized for different package managers:
|
||||||
|
|
||||||
|
- `Dockerfile` - for npm
|
||||||
|
- `Dockerfile.pnpm` - for pnpm
|
||||||
|
- `Dockerfile.bun` - for bun
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For npm
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# For pnpm
|
||||||
|
docker build -f Dockerfile.pnpm -t my-app .
|
||||||
|
|
||||||
|
# For bun
|
||||||
|
docker build -f Dockerfile.bun -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
1
apps/remix/app/app.css
Normal file
1
apps/remix/app/app.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '@documenso/ui/styles/theme.css';
|
||||||
74
apps/remix/app/root.tsx
Normal file
74
apps/remix/app/root.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
isRouteErrorResponse,
|
||||||
|
} from 'react-router';
|
||||||
|
|
||||||
|
import type { Route } from './+types/root';
|
||||||
|
import stylesheet from './app.css?url';
|
||||||
|
|
||||||
|
export const links: Route.LinksFunction = () => [
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://fonts.gstatic.com',
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
{ rel: 'stylesheet', href: stylesheet },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = 'Oops!';
|
||||||
|
let details = 'An unexpected error occurred.';
|
||||||
|
let stack: string | undefined;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? '404' : 'Error';
|
||||||
|
details =
|
||||||
|
error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
|
||||||
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto p-4 pt-16">
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre className="w-full overflow-x-auto p-4">
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
apps/remix/app/routes.ts
Normal file
3
apps/remix/app/routes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { type RouteConfig, index } from '@react-router/dev/routes';
|
||||||
|
|
||||||
|
export default [index('routes/home.tsx')] satisfies RouteConfig;
|
||||||
13
apps/remix/app/routes/home.tsx
Normal file
13
apps/remix/app/routes/home.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Welcome } from '../welcome/welcome';
|
||||||
|
import type { Route } from './+types/home';
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: 'New React Router App' },
|
||||||
|
{ name: 'description', content: 'Welcome to React Router!' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <Welcome />;
|
||||||
|
}
|
||||||
23
apps/remix/app/welcome/logo-dark.svg
Normal file
23
apps/remix/app/welcome/logo-dark.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.061 58.1641C71.1037 58.1641 58.1677 71.0742 58.1677 86.9996C58.1677 102.925 71.1037 115.835 87.061 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="white"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="white"/>
|
||||||
|
<path d="M289.314 144.671C289.314 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.314 160.596 289.314 144.671Z" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_202_2131)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.385 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.385 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="white"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="white"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="white"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="white"/>
|
||||||
|
<path d="M547.32 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.365 2.95282 554.365 13.1239C554.365 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.317 21.6426C553.595 22.8372 554.365 23.2391 554.365 30.0273V31.5345H547.332H547.32ZM522.457 18.3601H547.32V7.88763H522.457V18.349V18.3601Z" fill="white"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="white"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.746L631.335 31.5345H624.58L637.006 4.75034C637.71 3.22078 639.262 2.23828 640.936 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.283 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="white"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="white"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.147V7.02795H752.025V31.5345H745.282Z" fill="white"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.675 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_202_2131">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |
23
apps/remix/app/welcome/logo-light.svg
Normal file
23
apps/remix/app/welcome/logo-light.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.0608 58.1641C71.1035 58.1641 58.1676 71.0742 58.1676 86.9996C58.1676 102.925 71.1035 115.835 87.0608 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="#121212"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="#121212"/>
|
||||||
|
<path d="M289.313 144.671C289.313 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.313 160.596 289.313 144.671Z" fill="#121212"/>
|
||||||
|
<g clip-path="url(#clip0_171_1761)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.386 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.386 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="#121212"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="#121212"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="#121212"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="#121212"/>
|
||||||
|
<path d="M547.321 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.366 2.95282 554.366 13.1239C554.366 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.318 21.6426C553.595 22.8372 554.366 23.2391 554.366 30.0273V31.5345H547.332H547.321ZM522.457 18.3601H547.321V7.88763H522.457V18.349V18.3601Z" fill="#121212"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="#121212"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.747L631.335 31.5345H624.58L637.007 4.75034C637.71 3.22078 639.262 2.23828 640.937 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.284 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="#121212"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="#121212"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.148V7.02795H752.026V31.5345H745.282Z" fill="#121212"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.676 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="#121212"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_171_1761">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |
81
apps/remix/app/welcome/welcome.tsx
Normal file
81
apps/remix/app/welcome/welcome.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import logoDark from './logo-dark.svg';
|
||||||
|
import logoLight from './logo-light.svg';
|
||||||
|
|
||||||
|
export function Welcome() {
|
||||||
|
return (
|
||||||
|
<main className="flex items-center justify-center pb-4 pt-16">
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col items-center gap-16">
|
||||||
|
<header className="flex flex-col items-center gap-9">
|
||||||
|
<div className="w-[500px] max-w-[100vw] p-4">
|
||||||
|
<img src={logoLight} alt="React Router" className="block w-full dark:hidden" />
|
||||||
|
<img src={logoDark} alt="React Router" className="hidden w-full dark:block" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="w-full max-w-[300px] space-y-6 px-4">
|
||||||
|
<nav className="space-y-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
||||||
|
<p className="text-center leading-6 text-gray-700 dark:text-gray-200">
|
||||||
|
What's next?
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{resources.map(({ href, text, icon }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<a
|
||||||
|
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
href: 'https://reactrouter.com/docs',
|
||||||
|
text: 'React Router Docs',
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://rmx.as/discord',
|
||||||
|
text: 'Join Discord',
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 20"
|
||||||
|
fill="none"
|
||||||
|
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
33
apps/remix/package.json
Normal file
33
apps/remix/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@documenso/remix",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_ENV=production react-router build",
|
||||||
|
"dev": "tsx watch --ignore \"vite.config.{ts,mts,js,mjs}*\" server/main.ts",
|
||||||
|
"start": "cross-env NODE_ENV=production node dist/server/index.js",
|
||||||
|
"typecheck": "react-router typegen && tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/remember": "^1.1.0",
|
||||||
|
"@hono/node-server": "^1.13.7",
|
||||||
|
"@react-router/node": "^7.1.1",
|
||||||
|
"@react-router/serve": "^7.1.1",
|
||||||
|
"hono": "^4.6.15",
|
||||||
|
"isbot": "^5.1.17",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-router": "^7.1.1",
|
||||||
|
"remix-hono": "^0.0.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.1.1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"tsx": "^4.11.0",
|
||||||
|
"typescript": "5.7.2",
|
||||||
|
"vite": "^5.4.11",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/remix/public/favicon.ico
Normal file
BIN
apps/remix/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
apps/remix/react-router.config.ts
Normal file
7
apps/remix/react-router.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { Config } from '@react-router/dev/config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
appDirectory: 'app',
|
||||||
|
// Server-side render by default, to enable SPA mode set this to `false`
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
53
apps/remix/server/app.ts
Normal file
53
apps/remix/server/app.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { remember } from '@epic-web/remember';
|
||||||
|
import { type HttpBindings } from '@hono/node-server';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { reactRouter } from 'remix-hono/handler';
|
||||||
|
|
||||||
|
import { IS_APP_WEB } from '@documenso/lib/constants/app';
|
||||||
|
|
||||||
|
console.log({ IS_APP_WEB });
|
||||||
|
|
||||||
|
type Bindings = HttpBindings;
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
const viteDevServer = isProduction
|
||||||
|
? undefined
|
||||||
|
: import('vite').then(async (vite) =>
|
||||||
|
vite.createServer({
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reactRouterMiddleware = remember('reactRouterMiddleware', async () =>
|
||||||
|
reactRouter({
|
||||||
|
mode: isProduction ? 'production' : 'development',
|
||||||
|
build: isProduction
|
||||||
|
? // @ts-expect-error build/server/index.js is a build artifact
|
||||||
|
await import('../build/server/index.js')
|
||||||
|
: async () => (await viteDevServer)!.ssrLoadModule('virtual:react-router/server-build'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getApp = async () => {
|
||||||
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
|
|
||||||
|
const resolvedDevServer = await viteDevServer;
|
||||||
|
|
||||||
|
// app.get('/', (c) => c.text('Hello, world!'));
|
||||||
|
if (resolvedDevServer) {
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolvedDevServer.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
const middleware = await reactRouterMiddleware;
|
||||||
|
|
||||||
|
return middleware(c, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
};
|
||||||
16
apps/remix/server/main.ts
Normal file
16
apps/remix/server/main.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { serve } from '@hono/node-server';
|
||||||
|
|
||||||
|
import { getApp } from './app';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
serve(app, (info) => {
|
||||||
|
console.log(`Server is running on http://localhost:${info.port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
18
apps/remix/tailwind.config.ts
Normal file
18
apps/remix/tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const baseConfig = require('@documenso/tailwind-config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...baseConfig,
|
||||||
|
content: [
|
||||||
|
...baseConfig.content,
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/components/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/icons/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/lib/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/primitives/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/templates/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/template-components/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/providers/**/*.{ts,tsx}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
40
apps/remix/tsconfig.json
Normal file
40
apps/remix/tsconfig.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"**/.server/**/*",
|
||||||
|
"**/.client/**/*",
|
||||||
|
".react-router/types/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"rootDirs": [
|
||||||
|
".",
|
||||||
|
"./.react-router/types"
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"./app/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/remix/vite.config.mts
Normal file
15
apps/remix/vite.config.mts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// @ts-expect-error This is just due to our root config, it's a non-issue
|
||||||
|
import { reactRouter } from '@react-router/dev/vite';
|
||||||
|
import autoprefixer from 'autoprefixer';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss, autoprefixer],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [reactRouter(), tsconfigPaths()],
|
||||||
|
});
|
||||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@ -3,4 +3,4 @@
|
|||||||
/// <reference types="next/navigation-types/compat/navigation" />
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.7",
|
"version": "1.9.0-rc.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -33,12 +33,12 @@
|
|||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.5.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.10.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"perfect-freehand": "^1.2.0",
|
"perfect-freehand": "^1.2.0",
|
||||||
@ -68,11 +68,11 @@
|
|||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "20.1.0",
|
||||||
"@types/papaparse": "^5.3.14",
|
"@types/papaparse": "^5.3.14",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ export type DocumentPageViewButtonProps = {
|
|||||||
team?: Pick<Team, 'id' | 'url'>;
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -48,6 +48,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
const documentData = documentWithData?.documentData;
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
const documentData = documentWithData?.documentData;
|
||||||
|
|||||||
@ -125,7 +125,6 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
getFieldsForDocument({
|
getFieldsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const EditDocumentForm = ({
|
|||||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData: initialDocument,
|
initialData: initialDocument,
|
||||||
@ -73,12 +74,13 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
const { Recipient: recipients, Field: fields } = document;
|
const { Recipient: recipients, Field: fields } = document;
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
);
|
);
|
||||||
@ -92,6 +94,7 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||||
);
|
);
|
||||||
@ -104,18 +107,38 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
const { mutateAsync: updateTypedSignature } =
|
||||||
|
trpc.document.updateTypedSignatureSettings.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({
|
||||||
|
...(oldData || initialDocument),
|
||||||
|
...newData,
|
||||||
|
id: Number(newData.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ recipients: newRecipients }) => {
|
onSuccess: ({ recipients: newRecipients }) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
||||||
);
|
);
|
||||||
@ -128,6 +151,7 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
);
|
);
|
||||||
@ -181,8 +205,9 @@ export const EditDocumentForm = ({
|
|||||||
try {
|
try {
|
||||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||||
|
|
||||||
await updateDocument({
|
await setSettingsForDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
@ -221,9 +246,10 @@ export const EditDocumentForm = ({
|
|||||||
signingOrder: data.signingOrder,
|
signingOrder: data.signingOrder,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
addSigners({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipients: data.signers.map((signer) => ({
|
teamId: team?.id,
|
||||||
|
signers: data.signers.map((signer) => ({
|
||||||
...signer,
|
...signer,
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
actionAuth: signer.actionAuth || null,
|
actionAuth: signer.actionAuth || null,
|
||||||
@ -253,12 +279,9 @@ export const EditDocumentForm = ({
|
|||||||
fields: data.fields,
|
fields: data.fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateDocument({
|
await updateTypedSignature({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||||
meta: {
|
|
||||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
@ -290,6 +313,7 @@ export const EditDocumentForm = ({
|
|||||||
try {
|
try {
|
||||||
await sendDocument({
|
await sendDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
meta: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@ -15,7 +15,11 @@ export type DownloadAuditLogButtonProps = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
export const DownloadAuditLogButton = ({
|
||||||
|
className,
|
||||||
|
teamId,
|
||||||
|
documentId,
|
||||||
|
}: DownloadAuditLogButtonProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -24,7 +28,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
|
|||||||
|
|
||||||
const onDownloadAuditLogsClick = async () => {
|
const onDownloadAuditLogsClick = async () => {
|
||||||
try {
|
try {
|
||||||
const { url } = await downloadAuditLogs({ documentId });
|
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
src: url,
|
src: url,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const DownloadCertificateButton = ({
|
|||||||
|
|
||||||
const onDownloadCertificatesClick = async () => {
|
const onDownloadCertificatesClick = async () => {
|
||||||
try {
|
try {
|
||||||
const { url } = await downloadCertificate({ documentId });
|
const { url } = await downloadCertificate({ documentId, teamId });
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
src: url,
|
src: url,
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const ResendDocumentActionItem = ({
|
|||||||
|
|
||||||
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await resendDocument({ documentId: document.id, recipients });
|
await resendDocument({ documentId: document.id, recipients, teamId: team?.id });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document re-sent`),
|
title: _(msg`Document re-sent`),
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
|
|||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
document = await trpcClient.document.getDocumentById.query({
|
document = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
document = await trpcClient.document.getDocumentByToken.query({
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
document = await trpcClient.document.getDocumentById.query({
|
document = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
document = await trpcClient.document.getDocumentByToken.query({
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
status,
|
status,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
|
teamId,
|
||||||
canManageDocument,
|
canManageDocument,
|
||||||
}: DeleteDocumentDialogProps) => {
|
}: DeleteDocumentDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -75,7 +76,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDocument({ documentId: id });
|
await deleteDocument({ documentId: id, teamId });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||||
documentId: id,
|
documentId: id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const documentData = document?.documentData
|
||||||
@ -65,7 +66,7 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const onDuplicate = async () => {
|
const onDuplicate = async () => {
|
||||||
try {
|
try {
|
||||||
await duplicateDocument({ documentId: id });
|
await duplicateDocument({ documentId: id, teamId: team?.id });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
|||||||
const { id } = await createDocument({
|
const { id } = await createDocument({
|
||||||
title: file.name,
|
title: file.name,
|
||||||
documentDataId,
|
documentDataId,
|
||||||
|
teamId: team?.id,
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
|
|||||||
|
|
||||||
const { data } = trpc.template.findTemplates.useQuery({
|
const { data } = trpc.template.findTemplates.useQuery({
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
|
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
|
||||||
|
|||||||
@ -23,12 +23,15 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
|||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
|
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
type DirectTemplate = FindTemplateRow & {
|
type DirectTemplate = FindTemplateRow & {
|
||||||
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PublicTemplatesDataTable = () => {
|
export const PublicTemplatesDataTable = () => {
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -40,7 +43,9 @@ export const PublicTemplatesDataTable = () => {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
|
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
|
||||||
{},
|
{
|
||||||
|
teamId: team?.id,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export const EditTemplateForm = ({
|
|||||||
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
||||||
{
|
{
|
||||||
templateId: initialTemplate.id,
|
templateId: initialTemplate.id,
|
||||||
|
teamId: initialTemplate.teamId || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData: initialTemplate,
|
initialData: initialTemplate,
|
||||||
@ -103,6 +104,19 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSigningOrderForTemplate } =
|
||||||
|
trpc.template.setSigningOrderForTemplate.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.template.getTemplateById.setData(
|
||||||
|
{
|
||||||
|
templateId: initialTemplate.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
@ -115,7 +129,7 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
|
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.template.getTemplateById.setData(
|
utils.template.getTemplateById.setData(
|
||||||
@ -127,14 +141,31 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateTypedSignature } =
|
||||||
|
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.template.getTemplateById.setData(
|
||||||
|
{
|
||||||
|
templateId: initialTemplate.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({
|
||||||
|
...(oldData || initialTemplate),
|
||||||
|
...newData,
|
||||||
|
id: Number(newData.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? null,
|
||||||
},
|
},
|
||||||
@ -164,16 +195,16 @@ export const EditTemplateForm = ({
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updateTemplateSettings({
|
setSigningOrderForTemplate({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
meta: {
|
teamId: team?.id,
|
||||||
signingOrder: data.signingOrder,
|
signingOrder: data.signingOrder,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
addTemplateSigners({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
recipients: data.signers,
|
teamId: team?.id,
|
||||||
|
signers: data.signers,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -197,11 +228,10 @@ export const EditTemplateForm = ({
|
|||||||
fields: data.fields,
|
fields: data.fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateTemplateSettings({
|
await updateTypedSignature({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
meta: {
|
teamId: team?.id,
|
||||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
@ -266,7 +296,6 @@ export const EditTemplateForm = ({
|
|||||||
<AddTemplateSettingsFormPartial
|
<AddTemplateSettingsFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
template={template}
|
template={template}
|
||||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
|
||||||
documentFlow={documentFlow.settings}
|
documentFlow={documentFlow.settings}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export const TemplatePageViewDocumentsTable = ({
|
|||||||
trpc.document.findDocuments.useQuery(
|
trpc.document.findDocuments.useQuery(
|
||||||
{
|
{
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
page: parsedSearchParams.page,
|
page: parsedSearchParams.page,
|
||||||
perPage: parsedSearchParams.perPage,
|
perPage: parsedSearchParams.perPage,
|
||||||
query: parsedSearchParams.query,
|
query: parsedSearchParams.query,
|
||||||
|
|||||||
@ -20,10 +20,12 @@ export type TemplatePageViewRecentActivityProps = {
|
|||||||
|
|
||||||
export const TemplatePageViewRecentActivity = ({
|
export const TemplatePageViewRecentActivity = ({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
}: TemplatePageViewRecentActivityProps) => {
|
}: TemplatePageViewRecentActivityProps) => {
|
||||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId,
|
||||||
orderByColumn: 'createdAt',
|
orderByColumn: 'createdAt',
|
||||||
orderByDirection: 'asc',
|
orderByDirection: 'asc',
|
||||||
perPage: 5,
|
perPage: 5,
|
||||||
|
|||||||
@ -22,7 +22,12 @@ type DeleteTemplateDialogProps = {
|
|||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
export const DeleteTemplateDialog = ({
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DeleteTemplateDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -80,7 +85,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={async () => deleteTemplate({ templateId: id })}
|
onClick={async () => deleteTemplate({ templateId: id, teamId })}
|
||||||
>
|
>
|
||||||
<Trans>Delete</Trans>
|
<Trans>Delete</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type DuplicateTemplateDialogProps = {
|
|||||||
|
|
||||||
export const DuplicateTemplateDialog = ({
|
export const DuplicateTemplateDialog = ({
|
||||||
id,
|
id,
|
||||||
|
teamId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DuplicateTemplateDialogProps) => {
|
}: DuplicateTemplateDialogProps) => {
|
||||||
@ -83,6 +84,7 @@ export const DuplicateTemplateDialog = ({
|
|||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
duplicateTemplate({
|
duplicateTemplate({
|
||||||
templateId: id,
|
templateId: id,
|
||||||
|
teamId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ type NewTemplateDialogProps = {
|
|||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -58,6 +58,7 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
|
teamId,
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -174,6 +174,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
await createTemplateDirectLink({
|
await createTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
directRecipientId: recipientId,
|
directRecipientId: recipientId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -344,6 +345,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
createTemplateDirectLink({
|
createTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -7,17 +7,15 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
import { InfoIcon, Plus } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
|
||||||
import {
|
import {
|
||||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
||||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -47,14 +45,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z
|
||||||
.object({
|
.object({
|
||||||
distributeDocument: z.boolean(),
|
distributeDocument: z.boolean(),
|
||||||
useCustomDocument: z.boolean().default(false),
|
|
||||||
customDocumentData: z
|
|
||||||
.any()
|
|
||||||
.refine((data) => data instanceof File || data === undefined)
|
|
||||||
.optional(),
|
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@ -118,12 +113,12 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
distributeDocument: false,
|
distributeDocument: false,
|
||||||
useCustomDocument: false,
|
|
||||||
customDocumentData: undefined,
|
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@ -150,18 +145,11 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
||||||
try {
|
try {
|
||||||
let customDocumentDataId: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (data.useCustomDocument && data.customDocumentData) {
|
|
||||||
const customDocumentData = await putPdfFile(data.customDocumentData);
|
|
||||||
customDocumentDataId = customDocumentData.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await createDocumentFromTemplate({
|
const { id } = await createDocumentFromTemplate({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
recipients: data.recipients,
|
recipients: data.recipients,
|
||||||
distributeDocument: data.distributeDocument,
|
distributeDocument: data.distributeDocument,
|
||||||
customDocumentDataId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -312,245 +300,89 @@ export function UseTemplateDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{recipients.length > 0 && (
|
|
||||||
<div className="mt-4 flex flex-row items-center">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="distributeDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="distributeDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Send document</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
The document will be immediately sent to recipients if this
|
|
||||||
is checked.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Otherwise, the document will be created as a draft.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Create as pending</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Create the document as pending and ready to sign.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>We won't send anything to notify recipients.</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-2">
|
|
||||||
<Trans>
|
|
||||||
We will generate signing links for you, which you can send
|
|
||||||
to the recipients through your method of choice.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="useCustomDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="useCustomDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
field.onChange(checked);
|
|
||||||
if (!checked) {
|
|
||||||
form.setValue('customDocumentData', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="useCustomDocument"
|
|
||||||
>
|
|
||||||
<Trans>Upload custom document</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Upload a custom document to use instead of the template's default
|
|
||||||
document
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.watch('useCustomDocument') && (
|
|
||||||
<div className="my-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="customDocumentData"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<label
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground hover:border-muted-foreground/50 group relative flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 transition-colors',
|
|
||||||
{
|
|
||||||
'border-destructive hover:border-destructive':
|
|
||||||
form.formState.errors.customDocumentData,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
{!field.value && (
|
|
||||||
<>
|
|
||||||
<Upload className="text-muted-foreground/50 mx-auto h-10 w-10" />
|
|
||||||
<div className="mt-4 flex text-sm leading-6">
|
|
||||||
<span className="text-muted-foreground relative">
|
|
||||||
<Trans>
|
|
||||||
<span className="text-primary font-semibold">
|
|
||||||
Click to upload
|
|
||||||
</span>{' '}
|
|
||||||
or drag and drop
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground/80 text-xs">
|
|
||||||
PDF files only
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{field.value && (
|
|
||||||
<div className="text-muted-foreground space-y-1">
|
|
||||||
<p className="text-sm font-medium">{field.value.name}</p>
|
|
||||||
<p className="text-muted-foreground/60 text-xs">
|
|
||||||
{(field.value.size / (1024 * 1024)).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="absolute h-full w-full opacity-0"
|
|
||||||
accept=".pdf,application/pdf"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
field.onChange(undefined);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type !== 'application/pdf') {
|
|
||||||
form.setError('customDocumentData', {
|
|
||||||
type: 'manual',
|
|
||||||
message: _(msg`Please select a PDF file`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
|
|
||||||
form.setError('customDocumentData', {
|
|
||||||
type: 'manual',
|
|
||||||
message: _(
|
|
||||||
msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.onChange(file);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{field.value && (
|
|
||||||
<div className="absolute right-2 top-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="destructive"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
field.onChange(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<div className="sr-only">
|
|
||||||
<Trans>Clear file</Trans>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="mt-4">
|
{recipients.length > 0 && (
|
||||||
|
<div className="mt-4 flex flex-row items-center">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="distributeDocument"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="distributeDocument"
|
||||||
|
className="h-5 w-5"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Send document</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
The document will be immediately sent to recipients if this is
|
||||||
|
checked.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Otherwise, the document will be created as a draft.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Create as pending</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>Create the document as pending and ready to sign.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>We won't send anything to notify recipients.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>
|
||||||
|
We will generate signing links for you, which you can send to
|
||||||
|
the recipients through your method of choice.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary">
|
<Button type="button" variant="secondary">
|
||||||
<Trans>Close</Trans>
|
<Trans>Close</Trans>
|
||||||
|
|||||||
@ -66,7 +66,6 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
|||||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
userId: document.userId,
|
userId: document.userId,
|
||||||
teamId: document.teamId || undefined,
|
|
||||||
perPage: 100_000,
|
perPage: 100_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
|
|||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
|
||||||
|
|
||||||
import { Header } from '~/components/(dashboard)/layout/header';
|
import { Header } from '~/components/(dashboard)/layout/header';
|
||||||
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
||||||
@ -48,10 +47,6 @@ export default async function AuthenticatedTeamsLayout({
|
|||||||
const team = getTeamPromise.value;
|
const team = getTeamPromise.value;
|
||||||
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
|
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
|
||||||
|
|
||||||
const trpcHeaders = {
|
|
||||||
'x-team-Id': team.id.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider session={session}>
|
||||||
<LimitsProvider teamId={team.id}>
|
<LimitsProvider teamId={team.id}>
|
||||||
@ -66,9 +61,7 @@ export default async function AuthenticatedTeamsLayout({
|
|||||||
<Header user={user} teams={teams} />
|
<Header user={user} teams={teams} />
|
||||||
|
|
||||||
<TeamProvider team={team}>
|
<TeamProvider team={team}>
|
||||||
<TrpcProvider headers={trpcHeaders}>
|
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
||||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
|
||||||
</TrpcProvider>
|
|
||||||
</TeamProvider>
|
</TeamProvider>
|
||||||
|
|
||||||
<RefreshOnFocus />
|
<RefreshOnFocus />
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
type: TemplateType.PRIVATE,
|
type: TemplateType.PRIVATE,
|
||||||
},
|
},
|
||||||
@ -157,6 +158,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId: selectedTemplateId,
|
templateId: selectedTemplateId,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
type: TemplateType.PUBLIC,
|
type: TemplateType.PUBLIC,
|
||||||
publicTitle,
|
publicTitle,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const logger = buildLogger();
|
|||||||
export default createOpenApiNextHandler<typeof appRouter>({
|
export default createOpenApiNextHandler<typeof appRouter>({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
|
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
|
||||||
createTrpcContext({ req, res, requestSource: 'apiV2' }),
|
createTrpcContext({ req, res }),
|
||||||
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
|
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
|
||||||
// Always log the error for now.
|
// Always log the error for now.
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
@ -17,7 +17,7 @@ const logger = buildLogger();
|
|||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: async ({ req, res }) => createTrpcContext({ req, res, requestSource: 'app' }),
|
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
|
||||||
onError(opts) {
|
onError(opts) {
|
||||||
const { error, path } = opts;
|
const { error, path } = opts;
|
||||||
|
|
||||||
|
|||||||
15162
package-lock.json
generated
15162
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.0-rc.7",
|
"version": "1.9.0-rc.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
"dev": "turbo run dev --filter=@documenso/web",
|
"dev": "turbo run dev --filter=@documenso/web",
|
||||||
|
"dev:remix": "turbo run dev --filter=@documenso/remix",
|
||||||
"dev:web": "turbo run dev --filter=@documenso/web",
|
"dev:web": "turbo run dev --filter=@documenso/web",
|
||||||
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
||||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||||
@ -69,14 +70,14 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
"next": "14.2.23",
|
|
||||||
"next-runtime-env": "^3.2.0",
|
"next-runtime-env": "^3.2.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"typescript": "5.7.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.1",
|
"next": "14.2.6",
|
||||||
"next": "14.2.23"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"@ts-rest/next": "^3.30.5",
|
"@ts-rest/next": "^3.30.5",
|
||||||
"@ts-rest/open-api": "^3.33.0",
|
"@ts-rest/open-api": "^3.33.0",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"swagger-ui-react": "^5.11.0",
|
"swagger-ui-react": "^5.11.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { createNextRoute } from '@ts-rest/next';
|
import { createNextRoute } from '@ts-rest/next';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
@ -16,7 +15,8 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
|
|||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
|
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
@ -25,7 +25,7 @@ import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-for
|
|||||||
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
||||||
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
|
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||||
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
||||||
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
||||||
@ -36,10 +36,10 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
|
|||||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||||
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDropdownFieldMeta,
|
ZDropdownFieldMeta,
|
||||||
ZFieldMetaSchema,
|
|
||||||
ZNumberFieldMeta,
|
ZNumberFieldMeta,
|
||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
@ -53,7 +53,6 @@ import {
|
|||||||
} from '@documenso/lib/universal/upload/server-actions';
|
} from '@documenso/lib/universal/upload/server-actions';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { Prisma } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
@ -63,7 +62,6 @@ import {
|
|||||||
|
|
||||||
import { ApiContractV1 } from './contract';
|
import { ApiContractV1 } from './contract';
|
||||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||||
import { ZTemplateWithDataSchema } from './schema';
|
|
||||||
|
|
||||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||||
@ -98,14 +96,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
|
|
||||||
const recipients = await getRecipientsForDocument({
|
const recipients = await getRecipientsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = await getFieldsForDocument({
|
const fields = await getFieldsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedMetaFields = fields.map((field) => {
|
const parsedMetaFields = fields.map((field) => {
|
||||||
@ -210,7 +207,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -233,7 +230,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
id: document.id,
|
id: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
requestMetadata: metadata,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -250,7 +246,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body } = args;
|
const { body } = args;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -318,13 +314,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
formValues: body.formValues,
|
formValues: body.formValues,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
subject: body.meta.subject,
|
subject: body.meta.subject,
|
||||||
message: body.meta.message,
|
message: body.meta.message,
|
||||||
timezone,
|
timezone,
|
||||||
@ -335,7 +330,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||||
distributionMethod: body.meta.distributionMethod,
|
distributionMethod: body.meta.distributionMethod,
|
||||||
emailSettings: body.meta.emailSettings,
|
emailSettings: body.meta.emailSettings,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (body.authOptions) {
|
if (body.authOptions) {
|
||||||
@ -346,16 +341,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
data: {
|
data: {
|
||||||
...body.authOptions,
|
...body.authOptions,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recipients } = await setDocumentRecipients({
|
const { recipients } = await setRecipientsForDocument({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipients: body.recipients,
|
recipients: body.recipients,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -419,11 +414,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = ZTemplateWithDataSchema.parse(template);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: parsed,
|
body: template,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return AppError.toRestAPIError(err);
|
return AppError.toRestAPIError(err);
|
||||||
@ -442,12 +435,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = z.array(ZTemplateWithDataSchema).parse(templates);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
templates: parsed,
|
templates,
|
||||||
totalPages,
|
totalPages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -456,7 +447,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body, params } = args;
|
const { body, params } = args;
|
||||||
|
|
||||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||||
@ -520,9 +511,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
...body.meta,
|
...body.meta,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,7 +522,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
data: body.authOptions,
|
data: body.authOptions,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,7 +544,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body, params } = args;
|
const { body, params } = args;
|
||||||
|
|
||||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||||
@ -583,7 +573,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
title: body.title,
|
title: body.title,
|
||||||
...body.meta,
|
...body.meta,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return AppError.toRestAPIError(err);
|
return AppError.toRestAPIError(err);
|
||||||
@ -626,7 +615,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
data: body.authOptions,
|
data: body.authOptions,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +636,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { sendEmail, sendCompletionEmails } = args.body;
|
const { sendEmail, sendCompletionEmails } = args.body;
|
||||||
|
|
||||||
@ -683,13 +672,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
emailSettings: {
|
emailSettings: {
|
||||||
...emailSettings,
|
...emailSettings,
|
||||||
documentCompleted: sendCompletionEmails,
|
documentCompleted: sendCompletionEmails,
|
||||||
ownerDocumentCompleted: sendCompletionEmails,
|
ownerDocumentCompleted: sendCompletionEmails,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,7 +686,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
sendEmail,
|
sendEmail,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -722,7 +710,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
resendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { recipients } = args.body;
|
const { recipients } = args.body;
|
||||||
|
|
||||||
@ -732,7 +720,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
recipients,
|
recipients,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -751,7 +739,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||||
|
|
||||||
@ -797,7 +785,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { recipients: newRecipients } = await setDocumentRecipients({
|
const { recipients: newRecipients } = await setRecipientsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
@ -815,7 +803,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
actionAuth: authOptions?.actionAuth ?? null,
|
actionAuth: authOptions?.actionAuth ?? null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||||
@ -1580,39 +1568,3 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateDocument = async ({
|
|
||||||
documentId,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
documentId: number;
|
|
||||||
data: Prisma.DocumentUpdateInput;
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
}) => {
|
|
||||||
return await prisma.document.update({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import type { NextApiRequest } from 'next';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import type { Team, User } from '@documenso/prisma/client';
|
import type { Team, User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const authenticatedMiddleware = <
|
export const authenticatedMiddleware = <
|
||||||
@ -15,12 +13,7 @@ export const authenticatedMiddleware = <
|
|||||||
body: unknown;
|
body: unknown;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
handler: (
|
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
|
||||||
args: T,
|
|
||||||
user: User,
|
|
||||||
team: Team | null | undefined,
|
|
||||||
options: { metadata: ApiRequestMetadata },
|
|
||||||
) => Promise<R>,
|
|
||||||
) => {
|
) => {
|
||||||
return async (args: T) => {
|
return async (args: T) => {
|
||||||
try {
|
try {
|
||||||
@ -43,18 +36,7 @@ export const authenticatedMiddleware = <
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata: ApiRequestMetadata = {
|
return await handler(args, apiToken.user, apiToken.team);
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
||||||
source: 'apiV1',
|
|
||||||
auth: 'api',
|
|
||||||
auditUser: {
|
|
||||||
id: apiToken.team ? null : apiToken.user.id,
|
|
||||||
email: apiToken.team ? null : apiToken.user.email,
|
|
||||||
name: apiToken.team?.name ?? apiToken.user.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return await handler(args, apiToken.user, apiToken.team, { metadata });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log({ err: err });
|
console.log({ err: err });
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
|||||||
fields: z.lazy(() =>
|
fields: z.lazy(() =>
|
||||||
ZFieldSchema.pick({
|
ZFieldSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
documentId: true,
|
|
||||||
recipientId: true,
|
recipientId: true,
|
||||||
type: true,
|
type: true,
|
||||||
page: true,
|
page: true,
|
||||||
@ -69,8 +68,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
|||||||
positionY: true,
|
positionY: true,
|
||||||
width: true,
|
width: true,
|
||||||
height: true,
|
height: true,
|
||||||
customText: true,
|
|
||||||
fieldMeta: true,
|
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
fieldMeta: ZFieldMetaSchema.nullish(),
|
fieldMeta: ZFieldMetaSchema.nullish(),
|
||||||
@ -527,7 +524,6 @@ export const ZFieldSchema = z.object({
|
|||||||
height: z.unknown(),
|
height: z.unknown(),
|
||||||
customText: z.string(),
|
customText: z.string(),
|
||||||
inserted: z.boolean(),
|
inserted: z.boolean(),
|
||||||
fieldMeta: ZFieldMetaSchema.nullish().openapi({}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||||
@ -545,8 +541,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
|||||||
}),
|
}),
|
||||||
Field: ZFieldSchema.pick({
|
Field: ZFieldSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
documentId: true,
|
|
||||||
templateId: true,
|
|
||||||
recipientId: true,
|
recipientId: true,
|
||||||
type: true,
|
type: true,
|
||||||
page: true,
|
page: true,
|
||||||
@ -554,8 +548,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
|||||||
positionY: true,
|
positionY: true,
|
||||||
width: true,
|
width: true,
|
||||||
height: true,
|
height: true,
|
||||||
customText: true,
|
|
||||||
fieldMeta: true,
|
|
||||||
}).array(),
|
}).array(),
|
||||||
Recipient: ZRecipientSchema.pick({
|
Recipient: ZRecipientSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
@ -159,109 +157,3 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
|||||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set document visibility.
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Navigate back to the edit page to check that the settings are saved correctly.
|
|
||||||
await page.goto(`/t/${team.url}/templates/${template.id}/edit`);
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
|
|
||||||
const team = await seedTeam({
|
|
||||||
createTeamMembers: 2, // Create an additional member to test different roles
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.teamMember.update({
|
|
||||||
where: {
|
|
||||||
id: team.members[1].id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: TeamMemberRole.MANAGER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const owner = team.owner;
|
|
||||||
const managerUser = team.members[1].user;
|
|
||||||
const memberUser = team.members[2].user;
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test as manager
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: managerUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manager should be able to set visibility to managers and above
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
await expect(page.getByText('Admins only')).toBeDisabled();
|
|
||||||
|
|
||||||
// Save and verify
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Test as regular member
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: memberUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Regular member should not be able to modify visibility when set to managers and above
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
|
||||||
|
|
||||||
// Create a new template with 'everyone' visibility
|
|
||||||
const everyoneTemplate = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
visibility: 'EVERYONE',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to the new template
|
|
||||||
await page.goto(`/t/${team.url}/templates/${everyoneTemplate.id}/edit`);
|
|
||||||
|
|
||||||
// Regular member should be able to see but not modify visibility
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Everyone');
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import fs from 'fs';
|
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
@ -17,20 +13,6 @@ test.describe.configure({ mode: 'parallel' });
|
|||||||
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||||
|
|
||||||
// Create a temporary PDF file for testing
|
|
||||||
function createTempPdfFile() {
|
|
||||||
const tempDir = os.tmpdir();
|
|
||||||
const tempFilePath = path.join(tempDir, 'test.pdf');
|
|
||||||
|
|
||||||
// Create a simple PDF file with some content
|
|
||||||
const pdfContent = Buffer.from(
|
|
||||||
'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF',
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(tempFilePath, pdfContent);
|
|
||||||
return tempFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Create a template with all settings filled out
|
* 1. Create a template with all settings filled out
|
||||||
* 2. Create a document from the template
|
* 2. Create a document from the template
|
||||||
@ -301,318 +283,3 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that we can create a document from a template using a custom document
|
|
||||||
* instead of the template's default document.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a document from a template with custom document', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
// Create a temporary PDF file for upload
|
|
||||||
const testPdfPath = createTempPdfFile();
|
|
||||||
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template with custom document
|
|
||||||
await page.waitForURL('/templates');
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Enable custom document upload and upload file
|
|
||||||
await page.getByLabel('Upload custom document').check();
|
|
||||||
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
|
||||||
|
|
||||||
// Wait for upload to complete
|
|
||||||
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
|
||||||
|
|
||||||
// Create document with custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
|
||||||
expect(document.documentData.data).toEqual(pdfContent);
|
|
||||||
expect(document.documentData.initialData).toEqual(pdfContent);
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary file
|
|
||||||
fs.unlinkSync(testPdfPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that we can create a team document from a template using a custom document
|
|
||||||
* instead of the template's default document.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a team document from a template with custom document', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a temporary PDF file for upload
|
|
||||||
const testPdfPath = createTempPdfFile();
|
|
||||||
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template with custom document
|
|
||||||
await page.waitForURL(`/t/${team.url}/templates`);
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Enable custom document upload and upload file
|
|
||||||
await page.getByLabel('Upload custom document').check();
|
|
||||||
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
|
||||||
|
|
||||||
// Wait for upload to complete
|
|
||||||
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
|
||||||
|
|
||||||
// Create document with custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.teamId).toEqual(team.id);
|
|
||||||
expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
|
||||||
expect(document.documentData.data).toEqual(pdfContent);
|
|
||||||
expect(document.documentData.initialData).toEqual(pdfContent);
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary file
|
|
||||||
fs.unlinkSync(testPdfPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that when custom document upload is not enabled,
|
|
||||||
* the document uses the template's original document data.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a document from a template using template document when custom document is not enabled', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_ORIGINAL_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template without custom document
|
|
||||||
await page.waitForURL('/templates');
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Verify custom document upload is not checked by default
|
|
||||||
await expect(page.getByLabel('Upload custom document')).not.toBeChecked();
|
|
||||||
|
|
||||||
// Create document without custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the template's document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const templateWithData = await prisma.template.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: template.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
templateDocumentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
|
|
||||||
expect(document.documentData.data).toEqual(templateWithData.templateDocumentData.data);
|
|
||||||
expect(document.documentData.initialData).toEqual(
|
|
||||||
templateWithData.templateDocumentData.initialData,
|
|
||||||
);
|
|
||||||
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE]: should persist document visibility when creating from template', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title and visibility
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_VISIBILITY');
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Test creating document as team manager
|
|
||||||
await prisma.teamMember.update({
|
|
||||||
where: {
|
|
||||||
id: team.members[1].id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: TeamMemberRole.MANAGER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const managerUser = team.members[1].user;
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: managerUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the correct visibility
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_VISIBILITY');
|
|
||||||
expect(document.visibility).toEqual('MANAGER_AND_ABOVE');
|
|
||||||
expect(document.teamId).toEqual(team.id);
|
|
||||||
|
|
||||||
// Test that regular member cannot create document from restricted template
|
|
||||||
const memberUser = team.members[2].user;
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: memberUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Template should not be visible to regular member
|
|
||||||
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|||||||
@ -67,8 +67,6 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) =>
|
|||||||
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
|
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
|
||||||
await page.getByRole('button', { name: 'Create one automatically' }).click();
|
await page.getByRole('button', { name: 'Create one automatically' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
await page.getByTestId('btn-dialog-close').click();
|
await page.getByTestId('btn-dialog-close').click();
|
||||||
|
|
||||||
// Expect badge to appear.
|
// Expect badge to appear.
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.18.1",
|
"@playwright/test": "^1.18.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.8.2",
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@documenso/web": "*",
|
"@documenso/web": "*",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
This file lists all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
This file list all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||||
Copyright (c) 2023 Documenso, Inc
|
Copyright (c) 2023 Documenso, Inc
|
||||||
|
|
||||||
- The Stripe Billing Module
|
- The Stripe Billing Module
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
@ -15,6 +15,6 @@
|
|||||||
"eslint-plugin-package-json": "^0.10.4",
|
"eslint-plugin-package-json": "^0.10.4",
|
||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"eslint-plugin-unused-imports": "^3.1.0",
|
"eslint-plugin-unused-imports": "^3.1.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,10 +42,10 @@
|
|||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"inngest": "^3.19.13",
|
"inngest": "^3.19.13",
|
||||||
"kysely": "^0.26.3",
|
"kysely": "^0.26.3",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"oslo": "^0.17.0",
|
"oslo": "^0.17.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
diffDocumentMetaChanges,
|
diffDocumentMetaChanges,
|
||||||
@ -13,8 +13,6 @@ import type { SupportedLanguageCodes } from '../../constants/i18n';
|
|||||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
|
|
||||||
export type CreateDocumentMetaOptions = {
|
export type CreateDocumentMetaOptions = {
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@ -27,18 +25,18 @@ export type CreateDocumentMetaOptions = {
|
|||||||
distributionMethod?: DocumentDistributionMethod;
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
requestMetadata: ApiRequestMetadata;
|
userId: number;
|
||||||
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDocumentMeta = async ({
|
export const upsertDocumentMeta = async ({
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
timezone,
|
timezone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
documentId,
|
documentId,
|
||||||
password,
|
password,
|
||||||
|
userId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
emailSettings,
|
emailSettings,
|
||||||
@ -47,24 +45,34 @@ export const upsertDocumentMeta = async ({
|
|||||||
language,
|
language,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId: user.id,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
@ -112,7 +120,8 @@ export const upsertDocumentMeta = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||||
documentId,
|
documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { z } from 'zod';
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
@ -27,7 +27,7 @@ export type CreateDocumentOptions = {
|
|||||||
formValues?: Record<string, string | number | boolean>;
|
formValues?: Record<string, string | number | boolean>;
|
||||||
normalizePdf?: boolean;
|
normalizePdf?: boolean;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
||||||
@ -162,7 +162,8 @@ export const createDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
source: {
|
source: {
|
||||||
|
|||||||
@ -20,10 +20,9 @@ import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
|||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
@ -32,7 +31,7 @@ export type DeleteDocumentOptions = {
|
|||||||
id: number;
|
id: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteDocument = async ({
|
export const deleteDocument = async ({
|
||||||
@ -48,9 +47,7 @@ export const deleteDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new Error('User not found');
|
||||||
message: 'User not found',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
@ -70,9 +67,7 @@ export const deleteDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new Error('Document not found');
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserOwner = document.userId === userId;
|
const isUserOwner = document.userId === userId;
|
||||||
@ -80,9 +75,7 @@ export const deleteDocument = async ({
|
|||||||
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
||||||
|
|
||||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
throw new Error('Not allowed');
|
||||||
message: 'Not allowed',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hard or soft deleting the actual document if user has permission.
|
// Handle hard or soft deleting the actual document if user has permission.
|
||||||
@ -137,7 +130,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
|||||||
})
|
})
|
||||||
| null;
|
| null;
|
||||||
user: User;
|
user: User;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDocumentOwnerDelete = async ({
|
const handleDocumentOwnerDelete = async ({
|
||||||
@ -157,7 +150,8 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
type: 'SOFT',
|
type: 'SOFT',
|
||||||
},
|
},
|
||||||
@ -183,7 +177,8 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
type: 'HARD',
|
type: 'HARD',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|||||||
|
|
||||||
export interface FindDocumentAuditLogsOptions {
|
export interface FindDocumentAuditLogsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
@ -22,7 +21,6 @@ export interface FindDocumentAuditLogsOptions {
|
|||||||
|
|
||||||
export const findDocumentAuditLogs = async ({
|
export const findDocumentAuditLogs = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
documentId,
|
documentId,
|
||||||
page = 1,
|
page = 1,
|
||||||
perPage = 30,
|
perPage = 30,
|
||||||
@ -36,21 +34,20 @@ export const findDocumentAuditLogs = async ({
|
|||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ export type MoveDocumentToTeamOptions = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
||||||
@ -26,6 +26,10 @@ export const moveDocumentToTeam = async ({
|
|||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const user = await tx.user.findUniqueOrThrow({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
const document = await tx.document.findFirst({
|
const document = await tx.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -35,7 +39,8 @@ export const moveDocumentToTeam = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
message: 'Document not found or already associated with a team.',
|
message: 'Document not found or already associated with a team.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -52,8 +57,9 @@ export const moveDocumentToTeam = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
throw new TRPCError({
|
||||||
message: 'This team does not exist, or you are not a member of this team.',
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You are not a member of this team.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +68,12 @@ export const moveDocumentToTeam = async ({
|
|||||||
data: { teamId },
|
data: { teamId },
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
const log = await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||||
documentId: updatedDocument.id,
|
documentId: updatedDocument.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
movedByUserId: userId,
|
movedByUserId: userId,
|
||||||
fromPersonalAccount: true,
|
fromPersonalAccount: true,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
} from '@documenso/lib/constants/recipient-roles';
|
} from '@documenso/lib/constants/recipient-roles';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
recipients: number[];
|
recipients: number[];
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resendDocument = async ({
|
export const resendDocument = async ({
|
||||||
@ -201,7 +201,8 @@ export const resendDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
emailType: recipientEmailType,
|
emailType: recipientEmailType,
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
@ -31,7 +31,7 @@ export type SendDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
sendEmail?: boolean;
|
sendEmail?: boolean;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
||||||
@ -48,6 +48,17 @@ export const sendDocument = async ({
|
|||||||
sendEmail,
|
sendEmail,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
|
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -187,7 +198,7 @@ export const sendDocument = async ({
|
|||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
requestMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@ -204,7 +215,7 @@ export const sendDocument = async ({
|
|||||||
name: 'internal.seal-document',
|
name: 'internal.seal-document',
|
||||||
payload: {
|
payload: {
|
||||||
documentId,
|
documentId,
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
requestMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,7 +237,8 @@ export const sendDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
|
user,
|
||||||
data: {},
|
data: {},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
281
packages/lib/server-only/document/update-document-settings.ts
Normal file
281
packages/lib/server-only/document/update-document-settings.ts
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||||
|
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||||
|
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
|
|
||||||
|
export type UpdateDocumentSettingsOptions = {
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
documentId: number;
|
||||||
|
data: {
|
||||||
|
title?: string;
|
||||||
|
externalId?: string | null;
|
||||||
|
visibility?: DocumentVisibility | null;
|
||||||
|
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||||
|
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||||
|
};
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema;
|
||||||
|
|
||||||
|
export type TUpdateDocumentSettingsResponse = z.infer<typeof ZUpdateDocumentSettingsResponseSchema>;
|
||||||
|
|
||||||
|
export const updateDocumentSettings = async ({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
documentId,
|
||||||
|
data,
|
||||||
|
requestMetadata,
|
||||||
|
}: UpdateDocumentSettingsOptions): Promise<TUpdateDocumentSettingsResponse> => {
|
||||||
|
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Missing data to update',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
...(teamId
|
||||||
|
? {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userId,
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const currentUserRole = document.team?.members[0]?.role;
|
||||||
|
const isDocumentOwner = document.userId === userId;
|
||||||
|
const requestedVisibility = data.visibility;
|
||||||
|
|
||||||
|
if (!isDocumentOwner) {
|
||||||
|
match(currentUserRole)
|
||||||
|
.with(TeamMemberRole.ADMIN, () => true)
|
||||||
|
.with(TeamMemberRole.MANAGER, () => {
|
||||||
|
const allowedVisibilities: DocumentVisibility[] = [
|
||||||
|
DocumentVisibility.EVERYONE,
|
||||||
|
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowedVisibilities.includes(document.visibility) ||
|
||||||
|
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document visibility',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with(TeamMemberRole.MEMBER, () => {
|
||||||
|
if (
|
||||||
|
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||||
|
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document visibility',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.otherwise(() => {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: document.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||||
|
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||||
|
|
||||||
|
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||||
|
const newGlobalAccessAuth =
|
||||||
|
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||||
|
const newGlobalActionAuth =
|
||||||
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
|
// Check if user has permission to set the global action auth.
|
||||||
|
if (newGlobalActionAuth) {
|
||||||
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDocumentEnterprise) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to set the action auth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||||
|
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||||
|
const isGlobalAccessSame =
|
||||||
|
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||||
|
const isGlobalActionSame =
|
||||||
|
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||||
|
const isDocumentVisibilitySame =
|
||||||
|
data.visibility === undefined || data.visibility === document.visibility;
|
||||||
|
|
||||||
|
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||||
|
|
||||||
|
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'You cannot update the title if the document has been sent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTitleSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.title,
|
||||||
|
to: data.title || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExternalIdSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.externalId,
|
||||||
|
to: data.externalId || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalAccessSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: documentGlobalAccessAuth,
|
||||||
|
to: newGlobalAccessAuth,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalActionSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: documentGlobalActionAuth,
|
||||||
|
to: newGlobalActionAuth,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDocumentVisibilitySame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.visibility,
|
||||||
|
to: data.visibility || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return if nothing is required.
|
||||||
|
if (auditLogs.length === 0) {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const authOptions = createDocumentAuthOptions({
|
||||||
|
globalAccessAuth: newGlobalAccessAuth,
|
||||||
|
globalActionAuth: newGlobalActionAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedDocument = await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
externalId: data.externalId,
|
||||||
|
visibility: data.visibility as DocumentVisibility,
|
||||||
|
authOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.createMany({
|
||||||
|
data: auditLogs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedDocument;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,46 +1,23 @@
|
|||||||
import { match } from 'ts-pattern';
|
'use server';
|
||||||
import type { z } from 'zod';
|
|
||||||
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
|
||||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
|
||||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
||||||
|
|
||||||
export type UpdateDocumentOptions = {
|
export type UpdateDocumentOptions = {
|
||||||
|
documentId: number;
|
||||||
|
data: Prisma.DocumentUpdateInput;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
documentId: number;
|
|
||||||
data?: {
|
|
||||||
title?: string;
|
|
||||||
externalId?: string | null;
|
|
||||||
visibility?: DocumentVisibility | null;
|
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
|
||||||
};
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZUpdateDocumentResponseSchema = DocumentSchema;
|
|
||||||
|
|
||||||
export type TUpdateDocumentResponse = z.infer<typeof ZUpdateDocumentResponseSchema>;
|
|
||||||
|
|
||||||
export const updateDocument = async ({
|
export const updateDocument = async ({
|
||||||
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
|
||||||
data,
|
data,
|
||||||
requestMetadata,
|
}: UpdateDocumentOptions) => {
|
||||||
}: UpdateDocumentOptions): Promise<TUpdateDocumentResponse> => {
|
return await prisma.document.update({
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
...(teamId
|
||||||
@ -59,215 +36,8 @@ export const updateDocument = async ({
|
|||||||
teamId: null,
|
teamId: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
include: {
|
data: {
|
||||||
team: {
|
...data,
|
||||||
select: {
|
|
||||||
members: {
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (teamId) {
|
|
||||||
const currentUserRole = document.team?.members[0]?.role;
|
|
||||||
const isDocumentOwner = document.userId === userId;
|
|
||||||
const requestedVisibility = data?.visibility;
|
|
||||||
|
|
||||||
if (!isDocumentOwner) {
|
|
||||||
match(currentUserRole)
|
|
||||||
.with(TeamMemberRole.ADMIN, () => true)
|
|
||||||
.with(TeamMemberRole.MANAGER, () => {
|
|
||||||
const allowedVisibilities: DocumentVisibility[] = [
|
|
||||||
DocumentVisibility.EVERYONE,
|
|
||||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!allowedVisibilities.includes(document.visibility) ||
|
|
||||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document visibility',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with(TeamMemberRole.MEMBER, () => {
|
|
||||||
if (
|
|
||||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
|
||||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document visibility',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.otherwise(() => {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no data just return the document since this function is normally chained after a meta update.
|
|
||||||
if (!data || Object.values(data).length === 0) {
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
|
||||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
|
||||||
|
|
||||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
|
||||||
const newGlobalAccessAuth =
|
|
||||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
|
||||||
const newGlobalActionAuth =
|
|
||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (newGlobalActionAuth) {
|
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDocumentEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
|
||||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
|
||||||
const isGlobalAccessSame =
|
|
||||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
|
||||||
const isGlobalActionSame =
|
|
||||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
|
||||||
const isDocumentVisibilitySame =
|
|
||||||
data.visibility === undefined || data.visibility === document.visibility;
|
|
||||||
|
|
||||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
|
||||||
|
|
||||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
||||||
message: 'You cannot update the title if the document has been sent',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTitleSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.title,
|
|
||||||
to: data.title || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isExternalIdSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.externalId,
|
|
||||||
to: data.externalId || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGlobalAccessSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: documentGlobalAccessAuth,
|
|
||||||
to: newGlobalAccessAuth,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGlobalActionSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: documentGlobalActionAuth,
|
|
||||||
to: newGlobalActionAuth,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDocumentVisibilitySame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.visibility,
|
|
||||||
to: data.visibility || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return if nothing is required.
|
|
||||||
if (auditLogs.length === 0) {
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
|
||||||
const authOptions = createDocumentAuthOptions({
|
|
||||||
globalAccessAuth: newGlobalAccessAuth,
|
|
||||||
globalActionAuth: newGlobalActionAuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedDocument = await tx.document.update({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
externalId: data.externalId,
|
|
||||||
visibility: data.visibility as DocumentVisibility,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.createMany({
|
|
||||||
data: auditLogs,
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedDocument;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface CreateDocumentFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
fields: {
|
|
||||||
recipientId: number;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateDocumentFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateDocumentFieldsResponse = z.infer<typeof ZCreateDocumentFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const createDocumentFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
requestMetadata,
|
|
||||||
}: CreateDocumentFieldsOptions): Promise<TCreateDocumentFieldsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field validation.
|
|
||||||
const validatedFields = fields.map((field) => {
|
|
||||||
const recipient = document.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient ${field.recipientId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, document.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
validatedFields.map(async (field) => {
|
|
||||||
const createdField = await tx.field.create({
|
|
||||||
data: {
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.width,
|
|
||||||
height: field.height,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: field.fieldMeta,
|
|
||||||
documentId,
|
|
||||||
recipientId: field.recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle field created audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: createdField.secondaryId,
|
|
||||||
fieldRecipientEmail: field.recipientEmail,
|
|
||||||
fieldRecipientId: createdField.recipientId,
|
|
||||||
fieldType: createdField.type,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: createdFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface CreateTemplateFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
fields: {
|
|
||||||
recipientId: number;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateTemplateFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateTemplateFieldsResponse = z.infer<typeof ZCreateTemplateFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const createTemplateFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
fields,
|
|
||||||
}: CreateTemplateFieldsOptions): Promise<TCreateTemplateFieldsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field validation.
|
|
||||||
const validatedFields = fields.map((field) => {
|
|
||||||
const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient ${field.recipientId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, template.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Recipient type cannot have fields, or they have already interacted with the template.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
validatedFields.map(async (field) => {
|
|
||||||
const createdField = await tx.field.create({
|
|
||||||
data: {
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.width,
|
|
||||||
height: field.height,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: field.fieldMeta,
|
|
||||||
templateId,
|
|
||||||
recipientId: field.recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: createdFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface DeleteDocumentFieldOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
fieldId: number;
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteDocumentField = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
fieldId,
|
|
||||||
requestMetadata,
|
|
||||||
}: DeleteDocumentFieldOptions): Promise<void> => {
|
|
||||||
const field = await prisma.field.findFirst({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!field) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentId = field.documentId;
|
|
||||||
|
|
||||||
if (!documentId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field does not belong to a document. Use delete template field instead.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: field.recipientId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient for field ${fieldId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, recipient.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Recipient has already interacted with the document.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
const deletedField = await tx.field.delete({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle field deleted audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: deletedField.secondaryId,
|
|
||||||
fieldRecipientEmail: recipient.email,
|
|
||||||
fieldRecipientId: deletedField.recipientId,
|
|
||||||
fieldType: deletedField.type,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface DeleteTemplateFieldOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
fieldId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteTemplateField = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
fieldId,
|
|
||||||
}: DeleteTemplateFieldOptions): Promise<void> => {
|
|
||||||
const field = await prisma.field.findFirst({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
Template: teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!field || !field.templateId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.field.delete({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -3,34 +3,30 @@ import { prisma } from '@documenso/prisma';
|
|||||||
export interface GetFieldsForDocumentOptions {
|
export interface GetFieldsForDocumentOptions {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||||
|
|
||||||
export const getFieldsForDocument = async ({
|
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
||||||
documentId,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
}: GetFieldsForDocumentOptions) => {
|
|
||||||
const fields = await prisma.field.findMany({
|
const fields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Signature: true,
|
Signature: true,
|
||||||
|
|||||||
35
packages/lib/server-only/field/get-fields-for-template.ts
Normal file
35
packages/lib/server-only/field/get-fields-for-template.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export interface GetFieldsForTemplateOptions {
|
||||||
|
templateId: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForTemplateOptions) => {
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
templateId,
|
||||||
|
Template: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
diffFieldChanges,
|
diffFieldChanges,
|
||||||
@ -31,10 +31,9 @@ import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|||||||
|
|
||||||
export interface SetFieldsForDocumentOptions {
|
export interface SetFieldsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
fields: FieldData[];
|
fields: FieldData[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
||||||
@ -45,7 +44,6 @@ export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocument
|
|||||||
|
|
||||||
export const setFieldsForDocument = async ({
|
export const setFieldsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
documentId,
|
documentId,
|
||||||
fields,
|
fields,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
@ -53,27 +51,37 @@ export const setFieldsForDocument = async ({
|
|||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
message: 'Document not found',
|
message: 'Document not found',
|
||||||
@ -272,7 +280,8 @@ export const setFieldsForDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes,
|
changes,
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
@ -287,7 +296,8 @@ export const setFieldsForDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
},
|
},
|
||||||
@ -315,7 +325,8 @@ export const setFieldsForDocument = async ({
|
|||||||
createDocumentAuditLogData({
|
createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
fieldId: field.secondaryId,
|
fieldId: field.secondaryId,
|
||||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|||||||
|
|
||||||
export type SetFieldsForTemplateOptions = {
|
export type SetFieldsForTemplateOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
templateId: number;
|
||||||
fields: {
|
fields: {
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
@ -43,28 +42,26 @@ export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplate
|
|||||||
|
|
||||||
export const setFieldsForTemplate = async ({
|
export const setFieldsForTemplate = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
templateId,
|
templateId,
|
||||||
fields,
|
fields,
|
||||||
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,165 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import {
|
|
||||||
createDocumentAuditLogData,
|
|
||||||
diffFieldChanges,
|
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateDocumentFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
fields: {
|
|
||||||
id: number;
|
|
||||||
type?: FieldType;
|
|
||||||
pageNumber?: number;
|
|
||||||
pageX?: number;
|
|
||||||
pageY?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateDocumentFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateDocumentFieldsResponse = z.infer<typeof ZUpdateDocumentFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const updateDocumentFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
requestMetadata,
|
|
||||||
}: UpdateDocumentFieldsOptions): Promise<TUpdateDocumentFieldsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsToUpdate = fields.map((field) => {
|
|
||||||
const originalField = document.Field.find((existingField) => existingField.id === field.id);
|
|
||||||
|
|
||||||
if (!originalField) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Field with id ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.Recipient.find(
|
|
||||||
(recipient) => recipient.id === originalField.recipientId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient attached to field ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can be modified.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, document.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Cannot modify a field where the recipient has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalField,
|
|
||||||
updateData: field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
fieldsToUpdate.map(async ({ originalField, updateData, recipientEmail }) => {
|
|
||||||
const updatedField = await tx.field.update({
|
|
||||||
where: {
|
|
||||||
id: updateData.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: updateData.type,
|
|
||||||
page: updateData.pageNumber,
|
|
||||||
positionX: updateData.pageX,
|
|
||||||
positionY: updateData.pageY,
|
|
||||||
width: updateData.width,
|
|
||||||
height: updateData.height,
|
|
||||||
fieldMeta: updateData.fieldMeta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const changes = diffFieldChanges(originalField, updatedField);
|
|
||||||
|
|
||||||
// Handle field updated audit log.
|
|
||||||
if (changes.length > 0) {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: updatedField.secondaryId,
|
|
||||||
fieldRecipientEmail: recipientEmail,
|
|
||||||
fieldRecipientId: updatedField.recipientId,
|
|
||||||
fieldType: updatedField.type,
|
|
||||||
changes,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: updatedFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateTemplateFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
fields: {
|
|
||||||
id: number;
|
|
||||||
type?: FieldType;
|
|
||||||
pageNumber?: number;
|
|
||||||
pageX?: number;
|
|
||||||
pageY?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateTemplateFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateTemplateFieldsResponse = z.infer<typeof ZUpdateTemplateFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const updateTemplateFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
fields,
|
|
||||||
}: UpdateTemplateFieldsOptions): Promise<TUpdateTemplateFieldsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsToUpdate = fields.map((field) => {
|
|
||||||
const originalField = template.Field.find((existingField) => existingField.id === field.id);
|
|
||||||
|
|
||||||
if (!originalField) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Field with id ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = template.Recipient.find(
|
|
||||||
(recipient) => recipient.id === originalField.recipientId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient attached to field ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can be modified.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, template.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Cannot modify a field where the recipient has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateData: field,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
fieldsToUpdate.map(async ({ updateData }) => {
|
|
||||||
const updatedField = await tx.field.update({
|
|
||||||
where: {
|
|
||||||
id: updateData.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: updateData.type,
|
|
||||||
page: updateData.pageNumber,
|
|
||||||
positionX: updateData.pageX,
|
|
||||||
positionY: updateData.pageY,
|
|
||||||
width: updateData.width,
|
|
||||||
height: updateData.height,
|
|
||||||
fieldMeta: updateData.fieldMeta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: updatedFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -3,13 +3,13 @@ import sharp from 'sharp';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
|
|
||||||
export type SetAvatarImageOptions = {
|
export type SetAvatarImageOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number | null;
|
teamId?: number | null;
|
||||||
bytes?: string | null;
|
bytes?: string | null;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAvatarImage = async ({
|
export const setAvatarImage = async ({
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface CreateDocumentRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
recipients: {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientBaseResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateDocumentRecipientsResponse = z.infer<
|
|
||||||
typeof ZCreateDocumentRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createDocumentRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
recipients: recipientsToCreate,
|
|
||||||
requestMetadata,
|
|
||||||
}: CreateDocumentRecipientsOptions): Promise<TCreateDocumentRecipientsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
|
||||||
...recipient,
|
|
||||||
email: recipient.email.toLowerCase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
|
||||||
const existingRecipient = document.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return existingRecipient !== undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateRecipients.length > 0) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
normalizedRecipients.map(async (recipient) => {
|
|
||||||
const authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: recipient.accessAuth || null,
|
|
||||||
actionAuth: recipient.actionAuth || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
|
||||||
data: {
|
|
||||||
documentId,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
role: recipient.role,
|
|
||||||
signingOrder: recipient.signingOrder,
|
|
||||||
token: nanoid(),
|
|
||||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle recipient created audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: createdRecipient.email,
|
|
||||||
recipientName: createdRecipient.name,
|
|
||||||
recipientId: createdRecipient.id,
|
|
||||||
recipientRole: createdRecipient.role,
|
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: createdRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface CreateTemplateRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
recipients: {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientBaseResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateTemplateRecipientsResponse = z.infer<
|
|
||||||
typeof ZCreateTemplateRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createTemplateRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
recipients: recipientsToCreate,
|
|
||||||
}: CreateTemplateRecipientsOptions): Promise<TCreateTemplateRecipientsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
|
||||||
...recipient,
|
|
||||||
email: recipient.email.toLowerCase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
|
||||||
const existingRecipient = template.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return existingRecipient !== undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateRecipients.length > 0) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
normalizedRecipients.map(async (recipient) => {
|
|
||||||
const authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: recipient.accessAuth || null,
|
|
||||||
actionAuth: recipient.actionAuth || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
|
||||||
data: {
|
|
||||||
templateId,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
role: recipient.role,
|
|
||||||
signingOrder: recipient.signingOrder,
|
|
||||||
token: nanoid(),
|
|
||||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: createdRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { SendStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
|
|
||||||
export interface DeleteDocumentRecipientOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
recipientId: number;
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteDocumentRecipient = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
recipientId,
|
|
||||||
requestMetadata,
|
|
||||||
}: DeleteDocumentRecipientOptions): Promise<void> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
Recipient: {
|
|
||||||
some: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
team: true,
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'User not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientToDelete = document.Recipient[0];
|
|
||||||
|
|
||||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Recipient not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
await tx.recipient.delete({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
|
||||||
documentId: document.id,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: recipientToDelete.email,
|
|
||||||
recipientName: recipientToDelete.name,
|
|
||||||
recipientId: recipientToDelete.id,
|
|
||||||
recipientRole: recipientToDelete.role,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
||||||
document.documentMeta,
|
|
||||||
).recipientRemoved;
|
|
||||||
|
|
||||||
// Send email to deleted recipient.
|
|
||||||
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
|
|
||||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
inviterName: document.team?.name || user.name || undefined,
|
|
||||||
assetBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
|
|
||||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipientToDelete.email,
|
|
||||||
name: recipientToDelete.name,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`You have been removed from a document`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface DeleteTemplateRecipientOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
recipientId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteTemplateRecipient = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
recipientId,
|
|
||||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
Recipient: {
|
|
||||||
some: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientToDelete = template.Recipient[0];
|
|
||||||
|
|
||||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Recipient not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.recipient.delete({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -29,21 +29,25 @@ export const getRecipientById = async ({
|
|||||||
const recipient = await prisma.recipient.findFirst({
|
const recipient = await prisma.recipient.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: recipientId,
|
id: recipientId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
team: {
|
teamId === undefined
|
||||||
id: teamId,
|
? {
|
||||||
members: {
|
userId,
|
||||||
some: {
|
teamId: null,
|
||||||
userId,
|
}
|
||||||
|
: {
|
||||||
|
teamId,
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
}
|
},
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Field: true,
|
Field: true,
|
||||||
|
|||||||
@ -14,21 +14,23 @@ export const getRecipientsForDocument = async ({
|
|||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId,
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -3,32 +3,31 @@ import { prisma } from '@documenso/prisma';
|
|||||||
export interface GetRecipientsForTemplateOptions {
|
export interface GetRecipientsForTemplateOptions {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRecipientsForTemplate = async ({
|
export const getRecipientsForTemplate = async ({
|
||||||
templateId,
|
templateId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
}: GetRecipientsForTemplateOptions) => {
|
}: GetRecipientsForTemplateOptions) => {
|
||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
Template: teamId
|
Template: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-ent
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
import {
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
ZRecipientAuthOptionsSchema,
|
ZRecipientAuthOptionsSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
@ -34,27 +33,29 @@ import { canRecipientBeModified } from '../../utils/recipients';
|
|||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
|
|
||||||
export interface SetDocumentRecipientsOptions {
|
export interface SetRecipientsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
recipients: RecipientData[];
|
recipients: RecipientData[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZSetDocumentRecipientsResponseSchema = z.object({
|
export const ZSetRecipientsForDocumentResponseSchema = z.object({
|
||||||
recipients: RecipientSchema.array(),
|
recipients: RecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSetDocumentRecipientsResponse = z.infer<typeof ZSetDocumentRecipientsResponseSchema>;
|
export type TSetRecipientsForDocumentResponse = z.infer<
|
||||||
|
typeof ZSetRecipientsForDocumentResponseSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const setDocumentRecipients = async ({
|
export const setRecipientsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
documentId,
|
||||||
recipients,
|
recipients,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SetDocumentRecipientsOptions): Promise<TSetDocumentRecipientsResponse> => {
|
}: SetRecipientsForDocumentOptions): Promise<TSetRecipientsForDocumentResponse> => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -166,10 +167,10 @@ export const setDocumentRecipients = async ({
|
|||||||
linkedRecipients.map(async (recipient) => {
|
linkedRecipients.map(async (recipient) => {
|
||||||
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
|
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
|
||||||
|
|
||||||
if (recipient.actionAuth !== undefined || recipient.accessAuth !== undefined) {
|
if (recipient.actionAuth !== undefined) {
|
||||||
authOptions = createRecipientAuthOptions({
|
authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || authOptions.accessAuth,
|
accessAuth: authOptions.accessAuth,
|
||||||
actionAuth: recipient.actionAuth || authOptions.actionAuth,
|
actionAuth: recipient.actionAuth,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +236,8 @@ export const setDocumentRecipients = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes,
|
changes,
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
@ -250,10 +252,10 @@ export const setDocumentRecipients = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth || undefined,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -280,7 +282,8 @@ export const setDocumentRecipients = async ({
|
|||||||
createDocumentAuditLogData({
|
createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
@ -365,22 +368,17 @@ type RecipientData = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
recipient.email !== newRecipientData.email ||
|
recipient.email !== newRecipientData.email ||
|
||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
authOptions.actionAuth !== newRecipientData.actionAuth
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
import { nanoid } from '../../universal/id';
|
import { nanoid } from '../../universal/id';
|
||||||
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
||||||
|
|
||||||
export type SetTemplateRecipientsOptions = {
|
export type SetRecipientsForTemplateOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
templateId: number;
|
templateId: number;
|
||||||
@ -32,36 +32,37 @@ export type SetTemplateRecipientsOptions = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZSetTemplateRecipientsResponseSchema = z.object({
|
export const ZSetRecipientsForTemplateResponseSchema = z.object({
|
||||||
recipients: RecipientSchema.array(),
|
recipients: RecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSetTemplateRecipientsResponse = z.infer<typeof ZSetTemplateRecipientsResponseSchema>;
|
export type TSetRecipientsForTemplateResponse = z.infer<
|
||||||
|
typeof ZSetRecipientsForTemplateResponseSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const setTemplateRecipients = async ({
|
export const setRecipientsForTemplate = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
templateId,
|
templateId,
|
||||||
recipients,
|
recipients,
|
||||||
}: SetTemplateRecipientsOptions): Promise<TSetTemplateRecipientsResponse> => {
|
}: SetRecipientsForTemplateOptions): Promise<TSetRecipientsForTemplateResponse> => {
|
||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
directLink: true,
|
directLink: true,
|
||||||
@ -1,249 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
|
||||||
type TRecipientActionAuthTypes,
|
|
||||||
ZRecipientAuthOptionsSchema,
|
|
||||||
} from '@documenso/lib/types/document-auth';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import {
|
|
||||||
createDocumentAuditLogData,
|
|
||||||
diffRecipientChanges,
|
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateDocumentRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
recipients: RecipientData[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateDocumentRecipientsResponse = z.infer<
|
|
||||||
typeof ZUpdateDocumentRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const updateDocumentRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
recipients,
|
|
||||||
requestMetadata,
|
|
||||||
}: UpdateDocumentRecipientsOptions): Promise<TUpdateDocumentRecipientsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsToUpdate = recipients.map((recipient) => {
|
|
||||||
const originalRecipient = document.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!originalRecipient) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Recipient with id ${recipient.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateRecipientWithSameEmail = document.Recipient.find(
|
|
||||||
(existingRecipient) =>
|
|
||||||
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (duplicateRecipientWithSameEmail) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasRecipientBeenChanged(originalRecipient, recipient) &&
|
|
||||||
!canRecipientBeModified(originalRecipient, document.Field)
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalRecipient,
|
|
||||||
updateData: recipient,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
recipientsToUpdate.map(async ({ originalRecipient, updateData }) => {
|
|
||||||
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
|
|
||||||
|
|
||||||
if (updateData.actionAuth !== undefined || updateData.accessAuth !== undefined) {
|
|
||||||
authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: updateData.accessAuth || authOptions.accessAuth,
|
|
||||||
actionAuth: updateData.actionAuth || authOptions.actionAuth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedRecipient = {
|
|
||||||
...originalRecipient,
|
|
||||||
...updateData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedRecipient = await tx.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: originalRecipient.id,
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: mergedRecipient.name,
|
|
||||||
email: mergedRecipient.email,
|
|
||||||
role: mergedRecipient.role,
|
|
||||||
signingOrder: mergedRecipient.signingOrder,
|
|
||||||
documentId,
|
|
||||||
sendStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC
|
|
||||||
? SigningStatus.SIGNED
|
|
||||||
: SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear all fields if the recipient role is changed to a type that cannot have fields.
|
|
||||||
if (
|
|
||||||
originalRecipient.role !== updatedRecipient.role &&
|
|
||||||
(updatedRecipient.role === RecipientRole.CC ||
|
|
||||||
updatedRecipient.role === RecipientRole.VIEWER)
|
|
||||||
) {
|
|
||||||
await tx.field.deleteMany({
|
|
||||||
where: {
|
|
||||||
recipientId: updatedRecipient.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
|
|
||||||
|
|
||||||
// Handle recipient updated audit log.
|
|
||||||
if (changes.length > 0) {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: updatedRecipient.email,
|
|
||||||
recipientName: updatedRecipient.name,
|
|
||||||
recipientId: updatedRecipient.id,
|
|
||||||
recipientRole: updatedRecipient.role,
|
|
||||||
changes,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: updatedRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
|
||||||
*/
|
|
||||||
type RecipientData = {
|
|
||||||
id: number;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
role?: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
recipient.email !== newRecipientData.email ||
|
|
||||||
recipient.name !== newRecipientData.name ||
|
|
||||||
recipient.role !== newRecipientData.role ||
|
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
|
||||||
type TRecipientActionAuthTypes,
|
|
||||||
ZRecipientAuthOptionsSchema,
|
|
||||||
} from '@documenso/lib/types/document-auth';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface UpdateTemplateRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
recipients: {
|
|
||||||
id: number;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
role?: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateTemplateRecipientsResponse = z.infer<
|
|
||||||
typeof ZUpdateTemplateRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const updateTemplateRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
recipients,
|
|
||||||
}: UpdateTemplateRecipientsOptions): Promise<TUpdateTemplateRecipientsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsToUpdate = recipients.map((recipient) => {
|
|
||||||
const originalRecipient = template.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!originalRecipient) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Recipient with id ${recipient.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateRecipientWithSameEmail = template.Recipient.find(
|
|
||||||
(existingRecipient) =>
|
|
||||||
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (duplicateRecipientWithSameEmail) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalRecipient,
|
|
||||||
recipientUpdateData: recipient,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
recipientsToUpdate.map(async ({ originalRecipient, recipientUpdateData }) => {
|
|
||||||
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
|
|
||||||
|
|
||||||
if (
|
|
||||||
recipientUpdateData.actionAuth !== undefined ||
|
|
||||||
recipientUpdateData.accessAuth !== undefined
|
|
||||||
) {
|
|
||||||
authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: recipientUpdateData.accessAuth || authOptions.accessAuth,
|
|
||||||
actionAuth: recipientUpdateData.actionAuth || authOptions.actionAuth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedRecipient = {
|
|
||||||
...originalRecipient,
|
|
||||||
...recipientUpdateData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedRecipient = await tx.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: originalRecipient.id,
|
|
||||||
templateId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: mergedRecipient.name,
|
|
||||||
email: mergedRecipient.email,
|
|
||||||
role: mergedRecipient.role,
|
|
||||||
signingOrder: mergedRecipient.signingOrder,
|
|
||||||
templateId,
|
|
||||||
sendStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC
|
|
||||||
? SigningStatus.SIGNED
|
|
||||||
: SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear all fields if the recipient role is changed to a type that cannot have fields.
|
|
||||||
if (
|
|
||||||
originalRecipient.role !== updatedRecipient.role &&
|
|
||||||
(updatedRecipient.role === RecipientRole.CC ||
|
|
||||||
updatedRecipient.role === RecipientRole.VIEWER)
|
|
||||||
) {
|
|
||||||
await tx.field.deleteMany({
|
|
||||||
where: {
|
|
||||||
recipientId: updatedRecipient.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: updatedRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -33,7 +33,7 @@ import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
|||||||
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import {
|
import {
|
||||||
@ -55,7 +55,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
|
|||||||
directTemplateExternalId?: string;
|
directTemplateExternalId?: string;
|
||||||
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
||||||
templateUpdatedAt: Date;
|
templateUpdatedAt: Date;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: RequestMetadata;
|
||||||
user?: {
|
user?: {
|
||||||
id: number;
|
id: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -454,7 +454,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
name: user?.name,
|
name: user?.name,
|
||||||
email: directRecipientEmail,
|
email: directRecipientEmail,
|
||||||
},
|
},
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
title: document.title,
|
title: document.title,
|
||||||
source: {
|
source: {
|
||||||
@ -472,7 +472,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
name: user?.name,
|
name: user?.name,
|
||||||
email: directRecipientEmail,
|
email: directRecipientEmail,
|
||||||
},
|
},
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: createdDirectRecipient.email,
|
recipientEmail: createdDirectRecipient.email,
|
||||||
recipientId: createdDirectRecipient.id,
|
recipientId: createdDirectRecipient.id,
|
||||||
@ -490,7 +490,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
name: user?.name,
|
name: user?.name,
|
||||||
email: directRecipientEmail,
|
email: directRecipientEmail,
|
||||||
},
|
},
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: createdDirectRecipient.email,
|
recipientEmail: createdDirectRecipient.email,
|
||||||
recipientId: createdDirectRecipient.id,
|
recipientId: createdDirectRecipient.id,
|
||||||
@ -535,7 +535,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
name: user?.name,
|
name: user?.name,
|
||||||
email: directRecipientEmail,
|
email: directRecipientEmail,
|
||||||
},
|
},
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: createdDirectRecipient.email,
|
recipientEmail: createdDirectRecipient.email,
|
||||||
recipientId: createdDirectRecipient.id,
|
recipientId: createdDirectRecipient.id,
|
||||||
@ -609,7 +609,6 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentData: true,
|
documentData: true,
|
||||||
documentMeta: true,
|
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
|||||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import {
|
import {
|
||||||
createDocumentAuthOptions,
|
createDocumentAuthOptions,
|
||||||
@ -54,7 +54,6 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
}[];
|
}[];
|
||||||
customDocumentDataId?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Values that will override the predefined values in the template.
|
* Values that will override the predefined values in the template.
|
||||||
@ -73,7 +72,7 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
emailSettings?: TDocumentEmailSettings;
|
emailSettings?: TDocumentEmailSettings;
|
||||||
};
|
};
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentFromTemplateResponseSchema = DocumentSchema.extend({
|
export const ZCreateDocumentFromTemplateResponseSchema = DocumentSchema.extend({
|
||||||
@ -91,10 +90,15 @@ export const createDocumentFromTemplate = async ({
|
|||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
recipients,
|
recipients,
|
||||||
customDocumentDataId,
|
|
||||||
override,
|
override,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
|
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const template = await prisma.template.findUnique({
|
const template = await prisma.template.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
@ -167,29 +171,11 @@ export const createDocumentFromTemplate = async ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let parentDocumentData = template.templateDocumentData;
|
|
||||||
|
|
||||||
if (customDocumentDataId) {
|
|
||||||
const customDocumentData = await prisma.documentData.findFirst({
|
|
||||||
where: {
|
|
||||||
id: customDocumentDataId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customDocumentData) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Custom document data not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
parentDocumentData = customDocumentData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentData = await prisma.documentData.create({
|
const documentData = await prisma.documentData.create({
|
||||||
data: {
|
data: {
|
||||||
type: parentDocumentData.type,
|
type: template.templateDocumentData.type,
|
||||||
data: parentDocumentData.data,
|
data: template.templateDocumentData.data,
|
||||||
initialData: parentDocumentData.initialData,
|
initialData: template.templateDocumentData.initialData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,7 +193,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||||
}),
|
}),
|
||||||
visibility: template.visibility || template.team?.teamGlobalSettings?.documentVisibility,
|
visibility: template.team?.teamGlobalSettings?.documentVisibility,
|
||||||
documentMeta: {
|
documentMeta: {
|
||||||
create: {
|
create: {
|
||||||
subject: override?.subject || template.templateMeta?.subject,
|
subject: override?.subject || template.templateMeta?.subject,
|
||||||
@ -306,7 +292,8 @@ export const createDocumentFromTemplate = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
title: document.title,
|
title: document.title,
|
||||||
source: {
|
source: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user