mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
10 Commits
wip/rr7-au
...
chore/dece
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dbbed9ba8 | |||
| 08a69c6168 | |||
| 948d9c24cf | |||
| ebbe922982 | |||
| 6520bbd5e3 | |||
| 4e197ac24c | |||
| f707e5fb10 | |||
| 6fc5e565d0 | |||
| 07c852744b | |||
| 4fab98c633 |
@ -16,7 +16,7 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "14.2.6",
|
||||
"next": "14.2.23",
|
||||
"next-plausible": "^3.12.0",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
@ -27,6 +27,6 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "^5"
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
@ -13,11 +13,11 @@
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "14.2.6"
|
||||
"next": "14.2.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.16.5",
|
||||
"@types/react": "18.3.5",
|
||||
"typescript": "5.5.4"
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
9
apps/remix/.gitignore
vendored
9
apps/remix/.gitignore
vendored
@ -1,9 +0,0 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Vite
|
||||
vite.config.ts.timestamp*
|
||||
@ -1,22 +0,0 @@
|
||||
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"]
|
||||
@ -1,25 +0,0 @@
|
||||
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"]
|
||||
@ -1,26 +0,0 @@
|
||||
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"]
|
||||
@ -1,100 +0,0 @@
|
||||
# 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,8 +0,0 @@
|
||||
@import '@documenso/ui/styles/theme.css';
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: 'Inter';
|
||||
--font-signature: 'Caveat';
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
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: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400..600&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>
|
||||
);
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import { type RouteConfig, index } from '@react-router/dev/routes';
|
||||
|
||||
export default [index('routes/home.tsx')] satisfies RouteConfig;
|
||||
@ -1,19 +0,0 @@
|
||||
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 const loader = () => {
|
||||
return {
|
||||
message: 'Hello World' as const,
|
||||
};
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return <Welcome />;
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
@ -1,23 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
@ -1,81 +0,0 @@
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "@documenso/remix",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production react-router build",
|
||||
"dev": "tsx watch --ignore \"vite.config.ts*\" 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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@ -1,7 +0,0 @@
|
||||
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;
|
||||
@ -1,45 +0,0 @@
|
||||
import { remember } from '@epic-web/remember';
|
||||
import { type HttpBindings } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { reactRouter } from 'remix-hono/handler';
|
||||
|
||||
type Bindings = HttpBindings;
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const viteDevServer = isProduction
|
||||
? undefined
|
||||
: await 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 () => viteDevServer!.ssrLoadModule('virtual:react-router/server-build'),
|
||||
}),
|
||||
);
|
||||
|
||||
// app.get('/', (c) => c.text('Hello, world!'));
|
||||
if (viteDevServer) {
|
||||
app.use('*', async (c, next) => {
|
||||
return new Promise((resolve) => {
|
||||
viteDevServer.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
const middleware = await reactRouterMiddleware;
|
||||
|
||||
return middleware(c, next);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@ -1,7 +0,0 @@
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
import app from './app';
|
||||
|
||||
serve(app, (info) => {
|
||||
console.log(`Server is running on http://localhost:${info.port}`);
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
/* 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}`,
|
||||
],
|
||||
};
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"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,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.5",
|
||||
"version": "1.9.0-rc.7",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -33,12 +33,12 @@
|
||||
"framer-motion": "^10.12.8",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"luxon": "^3.5.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "14.2.23",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-plausible": "^3.10.1",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
@ -68,11 +68,11 @@
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
@ -25,7 +25,7 @@ export type DocumentPageViewButtonProps = {
|
||||
team?: Pick<Team, 'id' | 'url'>;
|
||||
};
|
||||
|
||||
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => {
|
||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
||||
const { data: session } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
@ -48,7 +48,6 @@ export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButto
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const documentData = documentWithData?.documentData;
|
||||
|
||||
@ -76,7 +76,6 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const documentData = documentWithData?.documentData;
|
||||
|
||||
@ -125,6 +125,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
getFieldsForDocument({
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@ -64,7 +64,6 @@ export const EditDocumentForm = ({
|
||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
{
|
||||
initialData: initialDocument,
|
||||
@ -74,13 +73,12 @@ export const EditDocumentForm = ({
|
||||
|
||||
const { Recipient: recipients, Field: fields } = document;
|
||||
|
||||
const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||
);
|
||||
@ -94,7 +92,6 @@ export const EditDocumentForm = ({
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||
);
|
||||
@ -107,38 +104,18 @@ export const EditDocumentForm = ({
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: ({ recipients: newRecipients }) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
||||
);
|
||||
@ -151,7 +128,6 @@ export const EditDocumentForm = ({
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||
);
|
||||
@ -205,9 +181,8 @@ export const EditDocumentForm = ({
|
||||
try {
|
||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||
|
||||
await setSettingsForDocument({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
@ -246,10 +221,9 @@ export const EditDocumentForm = ({
|
||||
signingOrder: data.signingOrder,
|
||||
}),
|
||||
|
||||
addSigners({
|
||||
setRecipients({
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
signers: data.signers.map((signer) => ({
|
||||
recipients: data.signers.map((signer) => ({
|
||||
...signer,
|
||||
// Explicitly set to null to indicate we want to remove auth if required.
|
||||
actionAuth: signer.actionAuth || null,
|
||||
@ -279,9 +253,12 @@ export const EditDocumentForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateTypedSignature({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
|
||||
meta: {
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
// Clear all field data from localStorage
|
||||
@ -313,7 +290,6 @@ export const EditDocumentForm = ({
|
||||
try {
|
||||
await sendDocument({
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
|
||||
@ -15,11 +15,7 @@ export type DownloadAuditLogButtonProps = {
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DownloadAuditLogButton = ({
|
||||
className,
|
||||
teamId,
|
||||
documentId,
|
||||
}: DownloadAuditLogButtonProps) => {
|
||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
@ -28,7 +24,7 @@ export const DownloadAuditLogButton = ({
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||
const { url } = await downloadAuditLogs({ documentId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@ -31,7 +31,7 @@ export const DownloadCertificateButton = ({
|
||||
|
||||
const onDownloadCertificatesClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadCertificate({ documentId, teamId });
|
||||
const { url } = await downloadCertificate({ documentId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@ -91,7 +91,7 @@ export const ResendDocumentActionItem = ({
|
||||
|
||||
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
||||
try {
|
||||
await resendDocument({ documentId: document.id, recipients, teamId: team?.id });
|
||||
await resendDocument({ documentId: document.id, recipients });
|
||||
|
||||
toast({
|
||||
title: _(msg`Document re-sent`),
|
||||
|
||||
@ -55,7 +55,6 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
|
||||
@ -86,7 +86,6 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
|
||||
@ -38,7 +38,6 @@ export const DeleteDocumentDialog = ({
|
||||
onOpenChange,
|
||||
status,
|
||||
documentTitle,
|
||||
teamId,
|
||||
canManageDocument,
|
||||
}: DeleteDocumentDialogProps) => {
|
||||
const router = useRouter();
|
||||
@ -76,7 +75,7 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteDocument({ documentId: id, teamId });
|
||||
await deleteDocument({ documentId: id });
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
|
||||
@ -37,7 +37,6 @@ export const DuplicateDocumentDialog = ({
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||
documentId: id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const documentData = document?.documentData
|
||||
@ -66,7 +65,7 @@ export const DuplicateDocumentDialog = ({
|
||||
|
||||
const onDuplicate = async () => {
|
||||
try {
|
||||
await duplicateDocument({ documentId: id, teamId: team?.id });
|
||||
await duplicateDocument({ documentId: id });
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
|
||||
@ -76,7 +76,6 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
const { id } = await createDocument({
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
teamId: team?.id,
|
||||
timezone: userTimezone,
|
||||
});
|
||||
|
||||
|
||||
@ -61,7 +61,6 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
|
||||
|
||||
const { data } = trpc.template.findTemplates.useQuery({
|
||||
perPage: 100,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
|
||||
|
||||
@ -23,15 +23,12 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
type DirectTemplate = FindTemplateRow & {
|
||||
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
||||
};
|
||||
|
||||
export const PublicTemplatesDataTable = () => {
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -43,9 +40,7 @@ export const PublicTemplatesDataTable = () => {
|
||||
} | null>(null);
|
||||
|
||||
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
|
||||
{
|
||||
teamId: team?.id,
|
||||
},
|
||||
{},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
},
|
||||
|
||||
@ -62,7 +62,6 @@ export const EditTemplateForm = ({
|
||||
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
||||
{
|
||||
templateId: initialTemplate.id,
|
||||
teamId: initialTemplate.teamId || undefined,
|
||||
},
|
||||
{
|
||||
initialData: initialTemplate,
|
||||
@ -104,19 +103,6 @@ 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({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
@ -129,7 +115,7 @@ export const EditTemplateForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
|
||||
const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateById.setData(
|
||||
@ -141,31 +127,14 @@ 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) => {
|
||||
try {
|
||||
await updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
@ -195,16 +164,16 @@ export const EditTemplateForm = ({
|
||||
) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
setSigningOrderForTemplate({
|
||||
updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
signingOrder: data.signingOrder,
|
||||
meta: {
|
||||
signingOrder: data.signingOrder,
|
||||
},
|
||||
}),
|
||||
|
||||
addTemplateSigners({
|
||||
setRecipients({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
signers: data.signers,
|
||||
recipients: data.signers,
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -228,10 +197,11 @@ export const EditTemplateForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateTypedSignature({
|
||||
await updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
meta: {
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
// Clear all field data from localStorage
|
||||
@ -296,6 +266,7 @@ export const EditTemplateForm = ({
|
||||
<AddTemplateSettingsFormPartial
|
||||
key={recipients.length}
|
||||
template={template}
|
||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
||||
documentFlow={documentFlow.settings}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
|
||||
@ -73,7 +73,6 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
trpc.document.findDocuments.useQuery(
|
||||
{
|
||||
templateId,
|
||||
teamId: team?.id,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
query: parsedSearchParams.query,
|
||||
|
||||
@ -20,12 +20,10 @@ export type TemplatePageViewRecentActivityProps = {
|
||||
|
||||
export const TemplatePageViewRecentActivity = ({
|
||||
templateId,
|
||||
teamId,
|
||||
documentRootPath,
|
||||
}: TemplatePageViewRecentActivityProps) => {
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||
templateId,
|
||||
teamId,
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
perPage: 5,
|
||||
|
||||
@ -22,12 +22,7 @@ type DeleteTemplateDialogProps = {
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const DeleteTemplateDialog = ({
|
||||
id,
|
||||
teamId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DeleteTemplateDialogProps) => {
|
||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
@ -85,7 +80,7 @@ export const DeleteTemplateDialog = ({
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={isLoading}
|
||||
onClick={async () => deleteTemplate({ templateId: id, teamId })}
|
||||
onClick={async () => deleteTemplate({ templateId: id })}
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
|
||||
@ -24,7 +24,6 @@ type DuplicateTemplateDialogProps = {
|
||||
|
||||
export const DuplicateTemplateDialog = ({
|
||||
id,
|
||||
teamId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DuplicateTemplateDialogProps) => {
|
||||
@ -84,7 +83,6 @@ export const DuplicateTemplateDialog = ({
|
||||
onClick={async () =>
|
||||
duplicateTemplate({
|
||||
templateId: id,
|
||||
teamId,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@ -31,7 +31,7 @@ type NewTemplateDialogProps = {
|
||||
templateRootPath: string;
|
||||
};
|
||||
|
||||
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => {
|
||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { data: session } = useSession();
|
||||
@ -58,7 +58,6 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
||||
});
|
||||
|
||||
const { id } = await createTemplate({
|
||||
teamId,
|
||||
title: file.name,
|
||||
templateDocumentDataId,
|
||||
});
|
||||
|
||||
@ -174,7 +174,6 @@ export const TemplateDirectLinkDialog = ({
|
||||
|
||||
await createTemplateDirectLink({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
directRecipientId: recipientId,
|
||||
});
|
||||
};
|
||||
@ -345,7 +344,6 @@ export const TemplateDirectLinkDialog = ({
|
||||
onClick={async () =>
|
||||
createTemplateDirectLink({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@ -7,15 +7,17 @@ import { useRouter } from 'next/navigation';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { InfoIcon, Plus } from 'lucide-react';
|
||||
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import {
|
||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||
} from '@documenso/lib/constants/template';
|
||||
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 { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@ -45,11 +47,14 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
const ZAddRecipientsForNewDocumentSchema = z
|
||||
.object({
|
||||
distributeDocument: z.boolean(),
|
||||
useCustomDocument: z.boolean().default(false),
|
||||
customDocumentData: z
|
||||
.any()
|
||||
.refine((data) => data instanceof File || data === undefined)
|
||||
.optional(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
@ -113,12 +118,12 @@ export function UseTemplateDialog({
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||
defaultValues: {
|
||||
distributeDocument: false,
|
||||
useCustomDocument: false,
|
||||
customDocumentData: undefined,
|
||||
recipients: recipients
|
||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||
.map((recipient) => {
|
||||
@ -145,11 +150,18 @@ export function UseTemplateDialog({
|
||||
|
||||
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
||||
try {
|
||||
let customDocumentDataId: string | undefined = undefined;
|
||||
|
||||
if (data.useCustomDocument && data.customDocumentData) {
|
||||
const customDocumentData = await putPdfFile(data.customDocumentData);
|
||||
customDocumentDataId = customDocumentData.id;
|
||||
}
|
||||
|
||||
const { id } = await createDocumentFromTemplate({
|
||||
templateId,
|
||||
teamId: team?.id,
|
||||
recipients: data.recipients,
|
||||
distributeDocument: data.distributeDocument,
|
||||
customDocumentDataId,
|
||||
});
|
||||
|
||||
toast({
|
||||
@ -300,89 +312,245 @@ export function UseTemplateDialog({
|
||||
/>
|
||||
</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>
|
||||
|
||||
{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>
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
<Trans>Close</Trans>
|
||||
|
||||
@ -66,6 +66,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||
documentId: documentId,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId || undefined,
|
||||
perPage: 100_000,
|
||||
});
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
|
||||
import { Header } from '~/components/(dashboard)/layout/header';
|
||||
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
||||
@ -47,6 +48,10 @@ export default async function AuthenticatedTeamsLayout({
|
||||
const team = getTeamPromise.value;
|
||||
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
|
||||
|
||||
const trpcHeaders = {
|
||||
'x-team-Id': team.id.toString(),
|
||||
};
|
||||
|
||||
return (
|
||||
<NextAuthProvider session={session}>
|
||||
<LimitsProvider teamId={team.id}>
|
||||
@ -61,7 +66,9 @@ export default async function AuthenticatedTeamsLayout({
|
||||
<Header user={user} teams={teams} />
|
||||
|
||||
<TeamProvider team={team}>
|
||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
||||
<TrpcProvider headers={trpcHeaders}>
|
||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
||||
</TrpcProvider>
|
||||
</TeamProvider>
|
||||
|
||||
<RefreshOnFocus />
|
||||
|
||||
@ -123,7 +123,6 @@ export const ManagePublicTemplateDialog = ({
|
||||
try {
|
||||
await updateTemplateSettings({
|
||||
templateId,
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
type: TemplateType.PRIVATE,
|
||||
},
|
||||
@ -158,7 +157,6 @@ export const ManagePublicTemplateDialog = ({
|
||||
try {
|
||||
await updateTemplateSettings({
|
||||
templateId: selectedTemplateId,
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
type: TemplateType.PUBLIC,
|
||||
publicTitle,
|
||||
|
||||
@ -17,7 +17,7 @@ const logger = buildLogger();
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
|
||||
createContext: async ({ req, res }) => createTrpcContext({ req, res, requestSource: 'app' }),
|
||||
onError(opts) {
|
||||
const { error, path } = opts;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const logger = buildLogger();
|
||||
export default createOpenApiNextHandler<typeof appRouter>({
|
||||
router: appRouter,
|
||||
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
|
||||
createTrpcContext({ req, res }),
|
||||
createTrpcContext({ req, res, requestSource: 'apiV2' }),
|
||||
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
|
||||
// Always log the error for now.
|
||||
console.error(error.message);
|
||||
15371
package-lock.json
generated
15371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,10 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.0-rc.5",
|
||||
"version": "1.9.0-rc.7",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --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:docs": "turbo run dev --filter=@documenso/documentation",
|
||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||
@ -70,14 +69,14 @@
|
||||
"inngest-cli": "^0.29.1",
|
||||
"luxon": "^3.5.0",
|
||||
"mupdf": "^1.0.0",
|
||||
"next": "14.2.23",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"react": "^18",
|
||||
"typescript": "5.7.2",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"overrides": {
|
||||
"next": "14.2.6",
|
||||
"zod": "3.24.1"
|
||||
"zod": "3.24.1",
|
||||
"next": "14.2.23"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
"@ts-rest/next": "^3.30.5",
|
||||
"@ts-rest/open-api": "^3.33.0",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"luxon": "^3.4.0",
|
||||
"luxon": "^3.5.0",
|
||||
"superjson": "^1.13.1",
|
||||
"swagger-ui-react": "^5.11.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { createNextRoute } from '@ts-rest/next';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
@ -15,8 +16,7 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
|
||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||
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 { 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 { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
||||
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 { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
@ -53,6 +53,7 @@ import {
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentDataType,
|
||||
DocumentStatus,
|
||||
@ -62,6 +63,7 @@ import {
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||
import { ZTemplateWithDataSchema } from './schema';
|
||||
|
||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||
@ -96,13 +98,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
teamId: team?.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const fields = await getFieldsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsedMetaFields = fields.map((field) => {
|
||||
@ -207,7 +210,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
@ -230,6 +233,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
id: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -246,7 +250,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body } = args;
|
||||
|
||||
try {
|
||||
@ -314,12 +318,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
formValues: body.formValues,
|
||||
documentDataId: documentData.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
subject: body.meta.subject,
|
||||
message: body.meta.message,
|
||||
timezone,
|
||||
@ -330,7 +335,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||
distributionMethod: body.meta.distributionMethod,
|
||||
emailSettings: body.meta.emailSettings,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
if (body.authOptions) {
|
||||
@ -341,16 +346,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
data: {
|
||||
...body.authOptions,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
const { recipients } = await setRecipientsForDocument({
|
||||
const { recipients } = await setDocumentRecipients({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
documentId: document.id,
|
||||
recipients: body.recipients,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -414,9 +419,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsed = ZTemplateWithDataSchema.parse(template);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: template,
|
||||
body: parsed,
|
||||
};
|
||||
} catch (err) {
|
||||
return AppError.toRestAPIError(err);
|
||||
@ -435,10 +442,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsed = z.array(ZTemplateWithDataSchema).parse(templates);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
templates,
|
||||
templates: parsed,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
@ -447,7 +456,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||
@ -511,8 +520,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
...body.meta,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -522,7 +532,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -544,7 +554,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||
@ -573,6 +583,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
title: body.title,
|
||||
...body.meta,
|
||||
},
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
return AppError.toRestAPIError(err);
|
||||
@ -615,7 +626,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -636,7 +647,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { sendEmail, sendCompletionEmails } = args.body;
|
||||
|
||||
@ -672,12 +683,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
emailSettings: {
|
||||
...emailSettings,
|
||||
documentCompleted: sendCompletionEmails,
|
||||
ownerDocumentCompleted: sendCompletionEmails,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -686,7 +698,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
sendEmail,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -710,7 +722,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
resendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { recipients } = args.body;
|
||||
|
||||
@ -720,7 +732,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
documentId: Number(documentId),
|
||||
recipients,
|
||||
teamId: team?.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -739,7 +751,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||
|
||||
@ -785,7 +797,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
|
||||
try {
|
||||
const { recipients: newRecipients } = await setRecipientsForDocument({
|
||||
const { recipients: newRecipients } = await setDocumentRecipients({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
@ -803,7 +815,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
actionAuth: authOptions?.actionAuth ?? null,
|
||||
},
|
||||
],
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||
@ -1568,3 +1580,39 @@ 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,6 +2,8 @@ import type { NextApiRequest } from 'next';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
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';
|
||||
|
||||
export const authenticatedMiddleware = <
|
||||
@ -13,7 +15,12 @@ export const authenticatedMiddleware = <
|
||||
body: unknown;
|
||||
},
|
||||
>(
|
||||
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
|
||||
handler: (
|
||||
args: T,
|
||||
user: User,
|
||||
team: Team | null | undefined,
|
||||
options: { metadata: ApiRequestMetadata },
|
||||
) => Promise<R>,
|
||||
) => {
|
||||
return async (args: T) => {
|
||||
try {
|
||||
@ -36,7 +43,18 @@ export const authenticatedMiddleware = <
|
||||
});
|
||||
}
|
||||
|
||||
return await handler(args, apiToken.user, apiToken.team);
|
||||
const metadata: ApiRequestMetadata = {
|
||||
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) {
|
||||
console.log({ err: err });
|
||||
|
||||
|
||||
@ -61,6 +61,7 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
||||
fields: z.lazy(() =>
|
||||
ZFieldSchema.pick({
|
||||
id: true,
|
||||
documentId: true,
|
||||
recipientId: true,
|
||||
type: true,
|
||||
page: true,
|
||||
@ -68,6 +69,8 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
||||
positionY: true,
|
||||
width: true,
|
||||
height: true,
|
||||
customText: true,
|
||||
fieldMeta: true,
|
||||
})
|
||||
.extend({
|
||||
fieldMeta: ZFieldMetaSchema.nullish(),
|
||||
@ -524,6 +527,7 @@ export const ZFieldSchema = z.object({
|
||||
height: z.unknown(),
|
||||
customText: z.string(),
|
||||
inserted: z.boolean(),
|
||||
fieldMeta: ZFieldMetaSchema.nullish().openapi({}),
|
||||
});
|
||||
|
||||
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||
@ -541,6 +545,8 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||
}),
|
||||
Field: ZFieldSchema.pick({
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
recipientId: true,
|
||||
type: true,
|
||||
page: true,
|
||||
@ -548,6 +554,8 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||
positionY: true,
|
||||
width: true,
|
||||
height: true,
|
||||
customText: true,
|
||||
fieldMeta: true,
|
||||
}).array(),
|
||||
Recipient: ZRecipientSchema.pick({
|
||||
id: true,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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 { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
@ -157,3 +159,109 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
||||
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,7 +1,11 @@
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
@ -13,6 +17,20 @@ test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
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
|
||||
* 2. Create a document from the template
|
||||
@ -283,3 +301,318 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
||||
expect(recipientOneAuth.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,6 +67,8 @@ 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: 'Create one automatically' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByTestId('btn-dialog-close').click();
|
||||
|
||||
// Expect badge to appear.
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.18.1",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/node": "^20",
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/web": "*",
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AuthenticationErrorCode } from './server/error-codes';
|
||||
import { AuthenticationError } from './server/errors';
|
||||
import { getSession } from './server/lib/session';
|
||||
|
||||
export const auth = new Hono();
|
||||
|
||||
auth.get('/session', async (c) => {
|
||||
const authorization = c.req.header('Authorization');
|
||||
|
||||
const userAgent = c.req.header('User-Agent');
|
||||
const ipAddress = c.req.header('X-Forwarded-For');
|
||||
|
||||
if (!authorization) {
|
||||
return new AuthenticationError(
|
||||
AuthenticationErrorCode.MissingToken,
|
||||
'Missing authorization header',
|
||||
).toHonoResponse(c);
|
||||
}
|
||||
|
||||
// Add your session validation logic here
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, prefer-const
|
||||
let { session, user } = await getSession(authorization);
|
||||
|
||||
const diff = DateTime.fromJSDate(session.expires).diffNow('days');
|
||||
|
||||
if (diff.days <= 3) {
|
||||
session = await prisma.session.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
expires: DateTime.now().plus({ days: 7 }).toJSDate(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
session,
|
||||
user,
|
||||
});
|
||||
});
|
||||
@ -1,4 +0,0 @@
|
||||
export * from './handler';
|
||||
export * from './server/errors';
|
||||
export * from './server/error-codes';
|
||||
export * from './server/middleware';
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "@documenso/auth",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"hono": "^4.6.15",
|
||||
"luxon": "^3.5.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "3.24.1"
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
|
||||
export const AuthenticationErrorCode = {
|
||||
Unauthorized: 'UNAUTHORIZED',
|
||||
InvalidCredentials: 'INVALID_CREDENTIALS',
|
||||
SessionNotFound: 'SESSION_NOT_FOUND',
|
||||
SessionExpired: 'SESSION_EXPIRED',
|
||||
InvalidToken: 'INVALID_TOKEN',
|
||||
MissingToken: 'MISSING_TOKEN',
|
||||
} as const;
|
||||
|
||||
export type AuthenticationErrorCode =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
(typeof AuthenticationErrorCode)[keyof typeof AuthenticationErrorCode] | (string & {});
|
||||
|
||||
export const ErrorStatusMap: Record<AuthenticationErrorCode, ContentfulStatusCode> = {
|
||||
[AuthenticationErrorCode.Unauthorized]: 401,
|
||||
[AuthenticationErrorCode.InvalidCredentials]: 401,
|
||||
[AuthenticationErrorCode.SessionNotFound]: 401,
|
||||
[AuthenticationErrorCode.SessionExpired]: 401,
|
||||
[AuthenticationErrorCode.InvalidToken]: 401,
|
||||
[AuthenticationErrorCode.MissingToken]: 400,
|
||||
};
|
||||
|
||||
export function getErrorStatus(code: AuthenticationErrorCode) {
|
||||
return ErrorStatusMap[code] ?? 400;
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
|
||||
import type { AuthenticationErrorCode } from './error-codes';
|
||||
import { getErrorStatus } from './error-codes';
|
||||
|
||||
interface ErrorResponse {
|
||||
error: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
export class AuthenticationError extends Error {
|
||||
code: AuthenticationErrorCode;
|
||||
statusCode: ContentfulStatusCode;
|
||||
|
||||
constructor(code: AuthenticationErrorCode, message?: string, statusCode?: ContentfulStatusCode) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.name = 'AuthenticationError';
|
||||
// Use provided status code or look it up from the map
|
||||
this.statusCode = statusCode ?? getErrorStatus(code);
|
||||
}
|
||||
|
||||
toJSON(): ErrorResponse {
|
||||
return {
|
||||
error: this.code,
|
||||
message: this.message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: this.stack }),
|
||||
};
|
||||
}
|
||||
|
||||
toHonoResponse(c: Context) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
...this.toJSON(),
|
||||
},
|
||||
this.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AuthenticationErrorCode } from '../error-codes';
|
||||
import { AuthenticationError } from '../errors';
|
||||
|
||||
export const getSession = async (token: string) => {
|
||||
const result = await prisma.session.findUnique({
|
||||
where: {
|
||||
sessionToken: token,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new AuthenticationError(AuthenticationErrorCode.SessionNotFound);
|
||||
}
|
||||
|
||||
if (result.expires < new Date()) {
|
||||
throw new AuthenticationError(AuthenticationErrorCode.SessionExpired);
|
||||
}
|
||||
|
||||
const { user, ...session } = result;
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
};
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const sessionTokenId = customAlphabet('abcdefhiklmnorstuvwxz', 10);
|
||||
|
||||
export const createSessionToken = (length = 10) => `session_${sessionTokenId(length)}` as const;
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
This file list all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||
This file lists all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||
Copyright (c) 2023 Documenso, Inc
|
||||
|
||||
- The Stripe Billing Module
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.4.0",
|
||||
"luxon": "^3.5.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "14.2.23",
|
||||
"next-auth": "4.24.5",
|
||||
"react": "^18",
|
||||
"ts-pattern": "^5.0.5",
|
||||
|
||||
@ -15,6 +15,6 @@
|
||||
"eslint-plugin-package-json": "^0.10.4",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,10 +42,10 @@
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"inngest": "^3.19.13",
|
||||
"kysely": "^0.26.3",
|
||||
"luxon": "^3.4.0",
|
||||
"luxon": "^3.5.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "14.2.6",
|
||||
"next": "14.2.23",
|
||||
"next-auth": "4.24.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffDocumentMetaChanges,
|
||||
@ -13,6 +13,8 @@ import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
@ -25,18 +27,18 @@ export type CreateDocumentMetaOptions = {
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
userId: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const upsertDocumentMeta = async ({
|
||||
userId,
|
||||
teamId,
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
documentId,
|
||||
password,
|
||||
userId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
emailSettings,
|
||||
@ -45,34 +47,24 @@ export const upsertDocumentMeta = async ({
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -120,8 +112,7 @@ export const upsertDocumentMeta = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ import type { z } from 'zod';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
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 type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
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 { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
@ -27,7 +27,7 @@ export type CreateDocumentOptions = {
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
normalizePdf?: boolean;
|
||||
timezone?: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
||||
@ -162,8 +162,7 @@ export const createDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
|
||||
@ -20,9 +20,10 @@ import { DocumentStatus, 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 { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
@ -31,7 +32,7 @@ export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteDocument = async ({
|
||||
@ -47,7 +48,9 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
@ -67,7 +70,9 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
@ -75,7 +80,9 @@ export const deleteDocument = async ({
|
||||
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
throw new Error('Not allowed');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Not allowed',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hard or soft deleting the actual document if user has permission.
|
||||
@ -130,7 +137,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
})
|
||||
| null;
|
||||
user: User;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
@ -150,8 +157,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'SOFT',
|
||||
},
|
||||
@ -177,8 +183,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'HARD',
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@ import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -21,6 +22,7 @@ export interface FindDocumentAuditLogsOptions {
|
||||
|
||||
export const findDocumentAuditLogs = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
page = 1,
|
||||
perPage = 30,
|
||||
@ -34,20 +36,21 @@ export const findDocumentAuditLogs = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
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 { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
@ -12,7 +12,7 @@ export type MoveDocumentToTeamOptions = {
|
||||
documentId: number;
|
||||
teamId: number;
|
||||
userId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
||||
@ -26,10 +26,6 @@ export const moveDocumentToTeam = async ({
|
||||
requestMetadata,
|
||||
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
const document = await tx.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -39,8 +35,7 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found or already associated with a team.',
|
||||
});
|
||||
}
|
||||
@ -57,9 +52,8 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You are not a member of this team.',
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'This team does not exist, or you are not a member of this team.',
|
||||
});
|
||||
}
|
||||
|
||||
@ -68,12 +62,11 @@ export const moveDocumentToTeam = async ({
|
||||
data: { teamId },
|
||||
});
|
||||
|
||||
const log = await tx.documentAuditLog.create({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||
documentId: updatedDocument.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
movedByUserId: userId,
|
||||
fromPersonalAccount: true,
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId?: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
@ -201,8 +201,7 @@ export const resendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -31,7 +31,7 @@ export type SendDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
||||
@ -48,17 +48,6 @@ export const sendDocument = async ({
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: 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({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -198,7 +187,7 @@ export const sendDocument = async ({
|
||||
userId,
|
||||
documentId,
|
||||
recipientId: recipient.id,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
}),
|
||||
@ -215,7 +204,7 @@ export const sendDocument = async ({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
@ -237,8 +226,7 @@ export const sendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
user,
|
||||
metadata: requestMetadata,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -1,281 +0,0 @@
|
||||
'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,23 +1,46 @@
|
||||
'use server';
|
||||
|
||||
import type { Prisma } from '@prisma/client';
|
||||
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 { 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 { 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 = {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: 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 ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
}: UpdateDocumentOptions) => {
|
||||
return await prisma.document.update({
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions): Promise<TUpdateDocumentResponse> => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
@ -36,8 +59,215 @@ export const updateDocument = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
include: {
|
||||
team: {
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
148
packages/lib/server-only/field/create-document-fields.ts
Normal file
148
packages/lib/server-only/field/create-document-fields.ts
Normal file
@ -0,0 +1,148 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
122
packages/lib/server-only/field/create-template-fields.ts
Normal file
122
packages/lib/server-only/field/create-template-fields.ts
Normal file
@ -0,0 +1,122 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
122
packages/lib/server-only/field/delete-document-field.ts
Normal file
122
packages/lib/server-only/field/delete-document-field.ts
Normal file
@ -0,0 +1,122 @@
|
||||
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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
48
packages/lib/server-only/field/delete-template-field.ts
Normal file
48
packages/lib/server-only/field/delete-template-field.ts
Normal file
@ -0,0 +1,48 @@
|
||||
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,30 +3,34 @@ import { prisma } from '@documenso/prisma';
|
||||
export interface GetFieldsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||
|
||||
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
||||
export const getFieldsForDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetFieldsForDocumentOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
Document: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
Document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
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,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffFieldChanges,
|
||||
@ -31,9 +31,10 @@ import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
fields: FieldData[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
||||
@ -44,6 +45,7 @@ export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocument
|
||||
|
||||
export const setFieldsForDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
@ -51,37 +53,27 @@ export const setFieldsForDocument = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
@ -280,8 +272,7 @@ export const setFieldsForDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
...baseAuditLog,
|
||||
@ -296,8 +287,7 @@ export const setFieldsForDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
},
|
||||
@ -325,8 +315,7 @@ export const setFieldsForDocument = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
|
||||
@ -20,6 +20,7 @@ import { FieldSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id?: number | null;
|
||||
@ -42,26 +43,28 @@ export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplate
|
||||
|
||||
export const setFieldsForTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
165
packages/lib/server-only/field/update-document-fields.ts
Normal file
165
packages/lib/server-only/field/update-document-fields.ts
Normal file
@ -0,0 +1,165 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
129
packages/lib/server-only/field/update-template-fields.ts
Normal file
129
packages/lib/server-only/field/update-template-fields.ts
Normal file
@ -0,0 +1,129 @@
|
||||
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 { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
|
||||
export type SetAvatarImageOptions = {
|
||||
userId: number;
|
||||
teamId?: number | null;
|
||||
bytes?: string | null;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const setAvatarImage = async ({
|
||||
|
||||
167
packages/lib/server-only/recipient/create-document-recipients.ts
Normal file
167
packages/lib/server-only/recipient/create-document-recipients.ts
Normal file
@ -0,0 +1,167 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
139
packages/lib/server-only/recipient/create-template-recipients.ts
Normal file
139
packages/lib/server-only/recipient/create-template-recipients.ts
Normal file
@ -0,0 +1,139 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
161
packages/lib/server-only/recipient/delete-document-recipient.ts
Normal file
161
packages/lib/server-only/recipient/delete-document-recipient.ts
Normal file
@ -0,0 +1,161 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
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,25 +29,21 @@ export const getRecipientById = async ({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
Document: {
|
||||
OR: [
|
||||
teamId === undefined
|
||||
? {
|
||||
userId,
|
||||
teamId: null,
|
||||
}
|
||||
: {
|
||||
teamId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
Document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Field: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user