mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
2 Commits
chore/dece
...
wip/rr7-be
| Author | SHA1 | Date | |
|---|---|---|---|
| 071ce70292 | |||
| 866b036484 |
@ -16,7 +16,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"nextra": "^2.13.4",
|
"nextra": "^2.13.4",
|
||||||
"nextra-theme-docs": "^2.13.4",
|
"nextra-theme-docs": "^2.13.4",
|
||||||
@ -27,6 +27,6 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"typescript": "5.2.2"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13,11 +13,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"next": "14.2.23"
|
"next": "14.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "20.16.5",
|
||||||
"@types/react": "^18",
|
"@types/react": "18.3.5",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
apps/remix/.dockerignore
Normal file
4
apps/remix/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
9
apps/remix/.gitignore
vendored
Normal file
9
apps/remix/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.ts.timestamp*
|
||||||
22
apps/remix/Dockerfile
Normal file
22
apps/remix/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
25
apps/remix/Dockerfile.bun
Normal file
25
apps/remix/Dockerfile.bun
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM oven/bun:1 AS dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --production
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
26
apps/remix/Dockerfile.pnpm
Normal file
26
apps/remix/Dockerfile.pnpm
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM node:20-alpine AS dependencies-env
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --prod --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
100
apps/remix/README.md
Normal file
100
apps/remix/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
This template includes three Dockerfiles optimized for different package managers:
|
||||||
|
|
||||||
|
- `Dockerfile` - for npm
|
||||||
|
- `Dockerfile.pnpm` - for pnpm
|
||||||
|
- `Dockerfile.bun` - for bun
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For npm
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# For pnpm
|
||||||
|
docker build -f Dockerfile.pnpm -t my-app .
|
||||||
|
|
||||||
|
# For bun
|
||||||
|
docker build -f Dockerfile.bun -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
1
apps/remix/app/app.css
Normal file
1
apps/remix/app/app.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '@documenso/ui/styles/theme.css';
|
||||||
13
apps/remix/app/lib/auth-client.ts
Normal file
13
apps/remix/app/lib/auth-client.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { twoFactor } from 'better-auth/plugins';
|
||||||
|
import { createAuthClient } from 'better-auth/react';
|
||||||
|
|
||||||
|
import { passkeyClientPlugin } from './auth/passkey-plugin/client';
|
||||||
|
|
||||||
|
// make sure to import from better-auth/react
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
plugins: [twoFactor(), passkeyClientPlugin()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { signIn, signOut, useSession } = authClient;
|
||||||
112
apps/remix/app/lib/auth.server.ts
Normal file
112
apps/remix/app/lib/auth.server.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { compare, hash } from '@node-rs/bcrypt';
|
||||||
|
import { betterAuth } from 'better-auth';
|
||||||
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||||
|
import { twoFactor } from 'better-auth/plugins';
|
||||||
|
|
||||||
|
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { passkeyPlugin } from './auth/passkey-plugin';
|
||||||
|
|
||||||
|
// todo: import from @documenso/lib/constants/auth
|
||||||
|
export const SALT_ROUNDS = 12;
|
||||||
|
|
||||||
|
const passkeyOptions = getAuthenticatorOptions();
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
appName: 'Documenso',
|
||||||
|
plugins: [
|
||||||
|
twoFactor({
|
||||||
|
issuer: 'Documenso',
|
||||||
|
skipVerificationOnEnable: true,
|
||||||
|
// totpOptions: {
|
||||||
|
|
||||||
|
// },
|
||||||
|
schema: {
|
||||||
|
twoFactor: {
|
||||||
|
modelName: 'TwoFactor',
|
||||||
|
fields: {
|
||||||
|
userId: 'userId',
|
||||||
|
secret: 'secret',
|
||||||
|
backupCodes: 'backupCodes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// todo: add options
|
||||||
|
}),
|
||||||
|
passkeyPlugin(),
|
||||||
|
// passkey({
|
||||||
|
// rpID: passkeyOptions.rpId,
|
||||||
|
// rpName: passkeyOptions.rpName,
|
||||||
|
// origin: passkeyOptions.origin,
|
||||||
|
// schema: {
|
||||||
|
// passkey: {
|
||||||
|
// fields: {
|
||||||
|
// publicKey: 'credentialPublicKey',
|
||||||
|
// credentialID: 'credentialId',
|
||||||
|
// deviceType: 'credentialDeviceType',
|
||||||
|
// backedUp: 'credentialBackedUp',
|
||||||
|
// // transports: '',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
],
|
||||||
|
secret: 'secret', // todo
|
||||||
|
database: prismaAdapter(prisma, {
|
||||||
|
provider: 'postgresql',
|
||||||
|
}),
|
||||||
|
databaseHooks: {
|
||||||
|
account: {
|
||||||
|
create: {
|
||||||
|
before: (session) => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
...session,
|
||||||
|
accountId: session.accountId.toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
fields: {
|
||||||
|
token: 'sessionToken',
|
||||||
|
expiresAt: 'expires',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
fields: {
|
||||||
|
emailVerified: 'isEmailVerified',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
fields: {
|
||||||
|
providerId: 'provider',
|
||||||
|
accountId: 'providerAccountId',
|
||||||
|
refreshToken: 'refresh_token',
|
||||||
|
accessToken: 'access_token',
|
||||||
|
idToken: 'id_token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
generateId: false,
|
||||||
|
},
|
||||||
|
socialProviders: {
|
||||||
|
google: {
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false,
|
||||||
|
// maxPasswordLength: 128,
|
||||||
|
// minPasswordLength: 8,
|
||||||
|
password: {
|
||||||
|
hash: async (password) => hash(password, SALT_ROUNDS),
|
||||||
|
verify: async ({ hash, password }) => compare(password, hash),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
24
apps/remix/app/lib/auth/passkey-plugin/client.ts
Normal file
24
apps/remix/app/lib/auth/passkey-plugin/client.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { BetterAuthClientPlugin } from 'better-auth';
|
||||||
|
|
||||||
|
import type { passkeyPlugin } from './index';
|
||||||
|
|
||||||
|
type PasskeyPlugin = typeof passkeyPlugin;
|
||||||
|
|
||||||
|
export const passkeyClientPlugin = () => {
|
||||||
|
const passkeySignin = () => {
|
||||||
|
//
|
||||||
|
// credential: JSON.stringify(credential),
|
||||||
|
// callbackUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'passkeyPlugin',
|
||||||
|
getActions: () => ({
|
||||||
|
signIn: {
|
||||||
|
passkey: () => passkeySignin,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
$InferServerPlugin: {} as ReturnType<PasskeyPlugin>,
|
||||||
|
} satisfies BetterAuthClientPlugin;
|
||||||
|
};
|
||||||
165
apps/remix/app/lib/auth/passkey-plugin/index.ts
Normal file
165
apps/remix/app/lib/auth/passkey-plugin/index.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import type { BetterAuthPlugin } from 'better-auth';
|
||||||
|
import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
|
||||||
|
|
||||||
|
export const passkeyPlugin = () =>
|
||||||
|
({
|
||||||
|
id: 'passkeyPlugin',
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
fields: {
|
||||||
|
// twoFactorEnabled: {
|
||||||
|
// type: 'boolean',
|
||||||
|
// required: false,
|
||||||
|
// },
|
||||||
|
// twoFactorBackupCodes: {
|
||||||
|
// type: 'string',
|
||||||
|
// required: false,
|
||||||
|
// },
|
||||||
|
// twoFactorSecret: {
|
||||||
|
// type: 'string',
|
||||||
|
// required: false,
|
||||||
|
// },
|
||||||
|
// birthday: {
|
||||||
|
// type: 'date', // string, number, boolean, date
|
||||||
|
// required: true, // if the field should be required on a new record. (default: false)
|
||||||
|
// unique: false, // if the field should be unique. (default: false)
|
||||||
|
// reference: null, // if the field is a reference to another table. (default: null)
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
endpoints: {
|
||||||
|
authorize: createAuthEndpoint(
|
||||||
|
'/passkey/authorize',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
// use: [],
|
||||||
|
},
|
||||||
|
async (ctx) => {
|
||||||
|
const csrfToken = credentials?.csrfToken;
|
||||||
|
|
||||||
|
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedBodyCredential = JSON.parse(req.body?.credential);
|
||||||
|
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
|
||||||
|
} catch {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeToken = await prisma.anonymousVerificationToken
|
||||||
|
.delete({
|
||||||
|
where: {
|
||||||
|
id: csrfToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (!challengeToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (challengeToken.expiresAt < new Date()) {
|
||||||
|
throw new AppError(AppErrorCode.EXPIRED_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passkey = await prisma.passkey.findFirst({
|
||||||
|
where: {
|
||||||
|
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!passkey) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_SETUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = passkey.User;
|
||||||
|
|
||||||
|
const { rpId, origin } = getAuthenticatorOptions();
|
||||||
|
|
||||||
|
const verification = await verifyAuthenticationResponse({
|
||||||
|
response: requestBodyCrediential,
|
||||||
|
expectedChallenge: challengeToken.token,
|
||||||
|
expectedOrigin: origin,
|
||||||
|
expectedRPID: rpId,
|
||||||
|
authenticator: {
|
||||||
|
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
|
||||||
|
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
|
||||||
|
counter: Number(passkey.counter),
|
||||||
|
},
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
const requestMetadata = extractNextAuthRequestMetadata(req);
|
||||||
|
|
||||||
|
if (!verification?.verified) {
|
||||||
|
await prisma.userSecurityAuditLog.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
ipAddress: requestMetadata.ipAddress,
|
||||||
|
userAgent: requestMetadata.userAgent,
|
||||||
|
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.passkey.update({
|
||||||
|
where: {
|
||||||
|
id: passkey.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
lastUsedAt: new Date(),
|
||||||
|
counter: verification.authenticationInfo.newCounter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: Number(user.id),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
emailVerified: user.emailVerified?.toISOString() ?? null,
|
||||||
|
} satisfies User;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
before: [
|
||||||
|
{
|
||||||
|
matcher: (context) => context.path.startsWith('/sign-in/email'),
|
||||||
|
handler: createAuthMiddleware(async (ctx) => {
|
||||||
|
console.log('here...');
|
||||||
|
|
||||||
|
const { birthday } = ctx.body;
|
||||||
|
|
||||||
|
if ((!birthday) instanceof Date) {
|
||||||
|
throw new APIError('BAD_REQUEST', { message: 'Birthday must be of type Date.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5));
|
||||||
|
|
||||||
|
if (birthday >= fiveYearsAgo) {
|
||||||
|
throw new APIError('BAD_REQUEST', { message: 'User must be above 5 years old.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { context: ctx };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}) satisfies BetterAuthPlugin;
|
||||||
74
apps/remix/app/root.tsx
Normal file
74
apps/remix/app/root.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
isRouteErrorResponse,
|
||||||
|
} from 'react-router';
|
||||||
|
|
||||||
|
import type { Route } from './+types/root';
|
||||||
|
import stylesheet from './app.css?url';
|
||||||
|
|
||||||
|
export const links: Route.LinksFunction = () => [
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://fonts.gstatic.com',
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
{ rel: 'stylesheet', href: stylesheet },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = 'Oops!';
|
||||||
|
let details = 'An unexpected error occurred.';
|
||||||
|
let stack: string | undefined;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? '404' : 'Error';
|
||||||
|
details =
|
||||||
|
error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
|
||||||
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto p-4 pt-16">
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre className="w-full overflow-x-auto p-4">
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
apps/remix/app/routes.ts
Normal file
4
apps/remix/app/routes.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { type RouteConfig } from '@react-router/dev/routes';
|
||||||
|
import { flatRoutes } from '@react-router/fs-routes';
|
||||||
|
|
||||||
|
export default flatRoutes() satisfies RouteConfig;
|
||||||
193
apps/remix/app/routes/_index.tsx
Normal file
193
apps/remix/app/routes/_index.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { authClient, signOut, useSession } from '~/lib/auth-client';
|
||||||
|
import { auth } from '~/lib/auth.server';
|
||||||
|
|
||||||
|
import type { Route } from '../+types/root';
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: 'New React Router App' },
|
||||||
|
{ name: 'description', content: 'Welcome to React Router!' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
query: {
|
||||||
|
disableCookieCache: true,
|
||||||
|
},
|
||||||
|
headers: request.headers, // pass the headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||||
|
return {
|
||||||
|
session: authClient.getSession(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { data } = useSession();
|
||||||
|
|
||||||
|
const [email, setEmail] = useState('deepfriedcoconut@gmail.com');
|
||||||
|
const [password, setPassword] = useState('password');
|
||||||
|
|
||||||
|
const signIn = async () => {
|
||||||
|
await authClient.signIn.email(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: (ctx) => {
|
||||||
|
// show loading state
|
||||||
|
},
|
||||||
|
onSuccess: (ctx) => {
|
||||||
|
console.log('success');
|
||||||
|
// redirect to home
|
||||||
|
},
|
||||||
|
onError: (ctx) => {
|
||||||
|
console.log(ctx.error);
|
||||||
|
alert(ctx.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const signUp = async () => {
|
||||||
|
await authClient.signUp.email(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: (ctx) => {
|
||||||
|
// show loading state
|
||||||
|
},
|
||||||
|
onSuccess: (ctx) => {
|
||||||
|
console.log(ctx);
|
||||||
|
// redirect to home
|
||||||
|
},
|
||||||
|
onError: (ctx) => {
|
||||||
|
console.log(ctx.error);
|
||||||
|
alert(ctx.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center justify-center pb-4 pt-16">
|
||||||
|
<h1>Status: {data ? 'Authenticated' : 'Not Authenticated'}</h1>
|
||||||
|
|
||||||
|
{data ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p>Session data</p>
|
||||||
|
<p className="mt-2 max-w-2xl text-xs">{JSON.stringify(data, null, 2)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
authClient.twoFactor
|
||||||
|
.enable({
|
||||||
|
password: 'password', // user password required
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enable 2FA
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
authClient.twoFactor.disable({
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={() => signOut()}>signout</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="">
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
<input
|
||||||
|
className="border border-blue-500"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="border border-blue-500"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="submit" onClick={signIn}>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="border border-blue-500"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="border border-blue-500"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<button type="submit" onClick={signUp}>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
authClient.signIn.social({
|
||||||
|
provider: 'google',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await authClient.signIn.passkey();
|
||||||
|
console.log(response);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
passkey
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
apps/remix/app/routes/api.auth.$.ts
Normal file
12
apps/remix/app/routes/api.auth.$.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Adjust the path as necessary
|
||||||
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
|
||||||
|
|
||||||
|
import { auth } from '~/lib/auth.server';
|
||||||
|
|
||||||
|
export function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
return auth.handler(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function action({ request }: ActionFunctionArgs) {
|
||||||
|
return auth.handler(request);
|
||||||
|
}
|
||||||
40
apps/remix/app/routes/signin.tsx
Normal file
40
apps/remix/app/routes/signin.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { authClient } from '~/lib/auth-client';
|
||||||
|
|
||||||
|
export default function SignIn() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const signIn = async () => {
|
||||||
|
await authClient.signIn.email(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: (ctx) => {
|
||||||
|
// show loading state
|
||||||
|
},
|
||||||
|
onSuccess: (ctx) => {
|
||||||
|
// redirect to home
|
||||||
|
},
|
||||||
|
onError: (ctx) => {
|
||||||
|
alert(ctx.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
|
|
||||||
|
<button type="submit" onClick={signIn}>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
apps/remix/package.json
Normal file
43
apps/remix/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"@documenso/api": "*",
|
||||||
|
"@documenso/assets": "*",
|
||||||
|
"@documenso/ee": "*",
|
||||||
|
"@documenso/lib": "*",
|
||||||
|
"@documenso/prisma": "*",
|
||||||
|
"@documenso/trpc": "*",
|
||||||
|
"@documenso/ui": "*",
|
||||||
|
"@epic-web/remember": "^1.1.0",
|
||||||
|
"@hono/node-server": "^1.13.7",
|
||||||
|
"@react-router/fs-routes": "^7.1.1",
|
||||||
|
"@react-router/node": "^7.1.1",
|
||||||
|
"@react-router/serve": "^7.1.1",
|
||||||
|
"better-auth": "^1.1.9",
|
||||||
|
"hono": "^4.6.15",
|
||||||
|
"isbot": "^5.1.17",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-router": "^7.1.1",
|
||||||
|
"remix-hono": "^0.0.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.1.1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"tsx": "^4.11.0",
|
||||||
|
"typescript": "5.7.2",
|
||||||
|
"vite": "^5.4.11",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/remix/public/favicon.ico
Normal file
BIN
apps/remix/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
apps/remix/react-router.config.ts
Normal file
7
apps/remix/react-router.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { Config } from '@react-router/dev/config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
appDirectory: 'app',
|
||||||
|
// Server-side render by default, to enable SPA mode set this to `false`
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
45
apps/remix/server/app.ts
Normal file
45
apps/remix/server/app.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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;
|
||||||
7
apps/remix/server/main.ts
Normal file
7
apps/remix/server/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { serve } from '@hono/node-server';
|
||||||
|
|
||||||
|
import app from './app';
|
||||||
|
|
||||||
|
serve(app, (info) => {
|
||||||
|
console.log(`Server is running on http://localhost:${info.port}`);
|
||||||
|
});
|
||||||
18
apps/remix/tailwind.config.ts
Normal file
18
apps/remix/tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const baseConfig = require('@documenso/tailwind-config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...baseConfig,
|
||||||
|
content: [
|
||||||
|
...baseConfig.content,
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/components/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/icons/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/lib/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/ui'), '..')}/primitives/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/templates/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/template-components/**/*.{ts,tsx}`,
|
||||||
|
`${path.join(require.resolve('@documenso/email'), '..')}/providers/**/*.{ts,tsx}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
27
apps/remix/tsconfig.json
Normal file
27
apps/remix/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
apps/remix/vite.config.ts
Normal file
17
apps/remix/vite.config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['@node-rs/bcrypt'],
|
||||||
|
},
|
||||||
|
});
|
||||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@ -3,4 +3,4 @@
|
|||||||
/// <reference types="next/navigation-types/compat/navigation" />
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.7",
|
"version": "1.9.0-rc.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -33,12 +33,12 @@
|
|||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.5.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.10.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"perfect-freehand": "^1.2.0",
|
"perfect-freehand": "^1.2.0",
|
||||||
@ -68,11 +68,11 @@
|
|||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "20.1.0",
|
||||||
"@types/papaparse": "^5.3.14",
|
"@types/papaparse": "^5.3.14",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ export type DocumentPageViewButtonProps = {
|
|||||||
team?: Pick<Team, 'id' | 'url'>;
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -48,6 +48,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
const documentData = documentWithData?.documentData;
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
const documentData = documentWithData?.documentData;
|
||||||
|
|||||||
@ -125,7 +125,6 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
getFieldsForDocument({
|
getFieldsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const EditDocumentForm = ({
|
|||||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData: initialDocument,
|
initialData: initialDocument,
|
||||||
@ -73,12 +74,13 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
const { Recipient: recipients, Field: fields } = document;
|
const { Recipient: recipients, Field: fields } = document;
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
);
|
);
|
||||||
@ -92,6 +94,7 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||||
);
|
);
|
||||||
@ -104,18 +107,38 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
const { mutateAsync: updateTypedSignature } =
|
||||||
|
trpc.document.updateTypedSignatureSettings.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({
|
||||||
|
...(oldData || initialDocument),
|
||||||
|
...newData,
|
||||||
|
id: Number(newData.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ recipients: newRecipients }) => {
|
onSuccess: ({ recipients: newRecipients }) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
||||||
);
|
);
|
||||||
@ -128,6 +151,7 @@ export const EditDocumentForm = ({
|
|||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
},
|
},
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
);
|
);
|
||||||
@ -181,8 +205,9 @@ export const EditDocumentForm = ({
|
|||||||
try {
|
try {
|
||||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||||
|
|
||||||
await updateDocument({
|
await setSettingsForDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
@ -221,9 +246,10 @@ export const EditDocumentForm = ({
|
|||||||
signingOrder: data.signingOrder,
|
signingOrder: data.signingOrder,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
addSigners({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipients: data.signers.map((signer) => ({
|
teamId: team?.id,
|
||||||
|
signers: data.signers.map((signer) => ({
|
||||||
...signer,
|
...signer,
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
actionAuth: signer.actionAuth || null,
|
actionAuth: signer.actionAuth || null,
|
||||||
@ -253,12 +279,9 @@ export const EditDocumentForm = ({
|
|||||||
fields: data.fields,
|
fields: data.fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateDocument({
|
await updateTypedSignature({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||||
meta: {
|
|
||||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
@ -290,6 +313,7 @@ export const EditDocumentForm = ({
|
|||||||
try {
|
try {
|
||||||
await sendDocument({
|
await sendDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
meta: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@ -15,7 +15,11 @@ export type DownloadAuditLogButtonProps = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
export const DownloadAuditLogButton = ({
|
||||||
|
className,
|
||||||
|
teamId,
|
||||||
|
documentId,
|
||||||
|
}: DownloadAuditLogButtonProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -24,7 +28,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
|
|||||||
|
|
||||||
const onDownloadAuditLogsClick = async () => {
|
const onDownloadAuditLogsClick = async () => {
|
||||||
try {
|
try {
|
||||||
const { url } = await downloadAuditLogs({ documentId });
|
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
src: url,
|
src: url,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const DownloadCertificateButton = ({
|
|||||||
|
|
||||||
const onDownloadCertificatesClick = async () => {
|
const onDownloadCertificatesClick = async () => {
|
||||||
try {
|
try {
|
||||||
const { url } = await downloadCertificate({ documentId });
|
const { url } = await downloadCertificate({ documentId, teamId });
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
src: url,
|
src: url,
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const ResendDocumentActionItem = ({
|
|||||||
|
|
||||||
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await resendDocument({ documentId: document.id, recipients });
|
await resendDocument({ documentId: document.id, recipients, teamId: team?.id });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document re-sent`),
|
title: _(msg`Document re-sent`),
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
|
|||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
document = await trpcClient.document.getDocumentById.query({
|
document = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
document = await trpcClient.document.getDocumentByToken.query({
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
document = await trpcClient.document.getDocumentById.query({
|
document = await trpcClient.document.getDocumentById.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
document = await trpcClient.document.getDocumentByToken.query({
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
status,
|
status,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
|
teamId,
|
||||||
canManageDocument,
|
canManageDocument,
|
||||||
}: DeleteDocumentDialogProps) => {
|
}: DeleteDocumentDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -75,7 +76,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDocument({ documentId: id });
|
await deleteDocument({ documentId: id, teamId });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||||
documentId: id,
|
documentId: id,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const documentData = document?.documentData
|
||||||
@ -65,7 +66,7 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const onDuplicate = async () => {
|
const onDuplicate = async () => {
|
||||||
try {
|
try {
|
||||||
await duplicateDocument({ documentId: id });
|
await duplicateDocument({ documentId: id, teamId: team?.id });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
|||||||
const { id } = await createDocument({
|
const { id } = await createDocument({
|
||||||
title: file.name,
|
title: file.name,
|
||||||
documentDataId,
|
documentDataId,
|
||||||
|
teamId: team?.id,
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
|
|||||||
|
|
||||||
const { data } = trpc.template.findTemplates.useQuery({
|
const { data } = trpc.template.findTemplates.useQuery({
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
|
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
|
||||||
|
|||||||
@ -23,12 +23,15 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
|||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
|
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
type DirectTemplate = FindTemplateRow & {
|
type DirectTemplate = FindTemplateRow & {
|
||||||
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PublicTemplatesDataTable = () => {
|
export const PublicTemplatesDataTable = () => {
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -40,7 +43,9 @@ export const PublicTemplatesDataTable = () => {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
|
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
|
||||||
{},
|
{
|
||||||
|
teamId: team?.id,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export const EditTemplateForm = ({
|
|||||||
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
||||||
{
|
{
|
||||||
templateId: initialTemplate.id,
|
templateId: initialTemplate.id,
|
||||||
|
teamId: initialTemplate.teamId || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData: initialTemplate,
|
initialData: initialTemplate,
|
||||||
@ -103,6 +104,19 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSigningOrderForTemplate } =
|
||||||
|
trpc.template.setSigningOrderForTemplate.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.template.getTemplateById.setData(
|
||||||
|
{
|
||||||
|
templateId: initialTemplate.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
@ -115,7 +129,7 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
|
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.template.getTemplateById.setData(
|
utils.template.getTemplateById.setData(
|
||||||
@ -127,14 +141,31 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateTypedSignature } =
|
||||||
|
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.template.getTemplateById.setData(
|
||||||
|
{
|
||||||
|
templateId: initialTemplate.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({
|
||||||
|
...(oldData || initialTemplate),
|
||||||
|
...newData,
|
||||||
|
id: Number(newData.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? null,
|
||||||
},
|
},
|
||||||
@ -164,16 +195,16 @@ export const EditTemplateForm = ({
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updateTemplateSettings({
|
setSigningOrderForTemplate({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
meta: {
|
teamId: team?.id,
|
||||||
signingOrder: data.signingOrder,
|
signingOrder: data.signingOrder,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
addTemplateSigners({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
recipients: data.signers,
|
teamId: team?.id,
|
||||||
|
signers: data.signers,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -197,11 +228,10 @@ export const EditTemplateForm = ({
|
|||||||
fields: data.fields,
|
fields: data.fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateTemplateSettings({
|
await updateTypedSignature({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
meta: {
|
teamId: team?.id,
|
||||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
@ -266,7 +296,6 @@ export const EditTemplateForm = ({
|
|||||||
<AddTemplateSettingsFormPartial
|
<AddTemplateSettingsFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
template={template}
|
template={template}
|
||||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
|
||||||
documentFlow={documentFlow.settings}
|
documentFlow={documentFlow.settings}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export const TemplatePageViewDocumentsTable = ({
|
|||||||
trpc.document.findDocuments.useQuery(
|
trpc.document.findDocuments.useQuery(
|
||||||
{
|
{
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
page: parsedSearchParams.page,
|
page: parsedSearchParams.page,
|
||||||
perPage: parsedSearchParams.perPage,
|
perPage: parsedSearchParams.perPage,
|
||||||
query: parsedSearchParams.query,
|
query: parsedSearchParams.query,
|
||||||
|
|||||||
@ -20,10 +20,12 @@ export type TemplatePageViewRecentActivityProps = {
|
|||||||
|
|
||||||
export const TemplatePageViewRecentActivity = ({
|
export const TemplatePageViewRecentActivity = ({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
}: TemplatePageViewRecentActivityProps) => {
|
}: TemplatePageViewRecentActivityProps) => {
|
||||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId,
|
||||||
orderByColumn: 'createdAt',
|
orderByColumn: 'createdAt',
|
||||||
orderByDirection: 'asc',
|
orderByDirection: 'asc',
|
||||||
perPage: 5,
|
perPage: 5,
|
||||||
|
|||||||
@ -22,7 +22,12 @@ type DeleteTemplateDialogProps = {
|
|||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
export const DeleteTemplateDialog = ({
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DeleteTemplateDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -80,7 +85,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={async () => deleteTemplate({ templateId: id })}
|
onClick={async () => deleteTemplate({ templateId: id, teamId })}
|
||||||
>
|
>
|
||||||
<Trans>Delete</Trans>
|
<Trans>Delete</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type DuplicateTemplateDialogProps = {
|
|||||||
|
|
||||||
export const DuplicateTemplateDialog = ({
|
export const DuplicateTemplateDialog = ({
|
||||||
id,
|
id,
|
||||||
|
teamId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DuplicateTemplateDialogProps) => {
|
}: DuplicateTemplateDialogProps) => {
|
||||||
@ -83,6 +84,7 @@ export const DuplicateTemplateDialog = ({
|
|||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
duplicateTemplate({
|
duplicateTemplate({
|
||||||
templateId: id,
|
templateId: id,
|
||||||
|
teamId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ type NewTemplateDialogProps = {
|
|||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -58,6 +58,7 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
|
teamId,
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -174,6 +174,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
await createTemplateDirectLink({
|
await createTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
directRecipientId: recipientId,
|
directRecipientId: recipientId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -344,6 +345,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
createTemplateDirectLink({
|
createTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -7,17 +7,15 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
import { InfoIcon, Plus } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
|
||||||
import {
|
import {
|
||||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
||||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -47,14 +45,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z
|
||||||
.object({
|
.object({
|
||||||
distributeDocument: z.boolean(),
|
distributeDocument: z.boolean(),
|
||||||
useCustomDocument: z.boolean().default(false),
|
|
||||||
customDocumentData: z
|
|
||||||
.any()
|
|
||||||
.refine((data) => data instanceof File || data === undefined)
|
|
||||||
.optional(),
|
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@ -118,12 +113,12 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
distributeDocument: false,
|
distributeDocument: false,
|
||||||
useCustomDocument: false,
|
|
||||||
customDocumentData: undefined,
|
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@ -150,18 +145,11 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
||||||
try {
|
try {
|
||||||
let customDocumentDataId: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (data.useCustomDocument && data.customDocumentData) {
|
|
||||||
const customDocumentData = await putPdfFile(data.customDocumentData);
|
|
||||||
customDocumentDataId = customDocumentData.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await createDocumentFromTemplate({
|
const { id } = await createDocumentFromTemplate({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
recipients: data.recipients,
|
recipients: data.recipients,
|
||||||
distributeDocument: data.distributeDocument,
|
distributeDocument: data.distributeDocument,
|
||||||
customDocumentDataId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -312,245 +300,89 @@ export function UseTemplateDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{recipients.length > 0 && (
|
|
||||||
<div className="mt-4 flex flex-row items-center">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="distributeDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="distributeDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Send document</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
The document will be immediately sent to recipients if this
|
|
||||||
is checked.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Otherwise, the document will be created as a draft.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Create as pending</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Create the document as pending and ready to sign.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>We won't send anything to notify recipients.</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-2">
|
|
||||||
<Trans>
|
|
||||||
We will generate signing links for you, which you can send
|
|
||||||
to the recipients through your method of choice.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="useCustomDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="useCustomDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
field.onChange(checked);
|
|
||||||
if (!checked) {
|
|
||||||
form.setValue('customDocumentData', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="useCustomDocument"
|
|
||||||
>
|
|
||||||
<Trans>Upload custom document</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Upload a custom document to use instead of the template's default
|
|
||||||
document
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.watch('useCustomDocument') && (
|
|
||||||
<div className="my-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="customDocumentData"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<label
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground hover:border-muted-foreground/50 group relative flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 transition-colors',
|
|
||||||
{
|
|
||||||
'border-destructive hover:border-destructive':
|
|
||||||
form.formState.errors.customDocumentData,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
{!field.value && (
|
|
||||||
<>
|
|
||||||
<Upload className="text-muted-foreground/50 mx-auto h-10 w-10" />
|
|
||||||
<div className="mt-4 flex text-sm leading-6">
|
|
||||||
<span className="text-muted-foreground relative">
|
|
||||||
<Trans>
|
|
||||||
<span className="text-primary font-semibold">
|
|
||||||
Click to upload
|
|
||||||
</span>{' '}
|
|
||||||
or drag and drop
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground/80 text-xs">
|
|
||||||
PDF files only
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{field.value && (
|
|
||||||
<div className="text-muted-foreground space-y-1">
|
|
||||||
<p className="text-sm font-medium">{field.value.name}</p>
|
|
||||||
<p className="text-muted-foreground/60 text-xs">
|
|
||||||
{(field.value.size / (1024 * 1024)).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="absolute h-full w-full opacity-0"
|
|
||||||
accept=".pdf,application/pdf"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
field.onChange(undefined);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type !== 'application/pdf') {
|
|
||||||
form.setError('customDocumentData', {
|
|
||||||
type: 'manual',
|
|
||||||
message: _(msg`Please select a PDF file`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
|
|
||||||
form.setError('customDocumentData', {
|
|
||||||
type: 'manual',
|
|
||||||
message: _(
|
|
||||||
msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.onChange(file);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{field.value && (
|
|
||||||
<div className="absolute right-2 top-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="destructive"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
field.onChange(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<div className="sr-only">
|
|
||||||
<Trans>Clear file</Trans>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="mt-4">
|
{recipients.length > 0 && (
|
||||||
|
<div className="mt-4 flex flex-row items-center">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="distributeDocument"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="distributeDocument"
|
||||||
|
className="h-5 w-5"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Send document</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
The document will be immediately sent to recipients if this is
|
||||||
|
checked.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Otherwise, the document will be created as a draft.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Create as pending</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>Create the document as pending and ready to sign.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>We won't send anything to notify recipients.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>
|
||||||
|
We will generate signing links for you, which you can send to
|
||||||
|
the recipients through your method of choice.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary">
|
<Button type="button" variant="secondary">
|
||||||
<Trans>Close</Trans>
|
<Trans>Close</Trans>
|
||||||
|
|||||||
@ -66,7 +66,6 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
|||||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
userId: document.userId,
|
userId: document.userId,
|
||||||
teamId: document.teamId || undefined,
|
|
||||||
perPage: 100_000,
|
perPage: 100_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
|
|||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
|
||||||
|
|
||||||
import { Header } from '~/components/(dashboard)/layout/header';
|
import { Header } from '~/components/(dashboard)/layout/header';
|
||||||
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
||||||
@ -48,10 +47,6 @@ export default async function AuthenticatedTeamsLayout({
|
|||||||
const team = getTeamPromise.value;
|
const team = getTeamPromise.value;
|
||||||
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
|
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
|
||||||
|
|
||||||
const trpcHeaders = {
|
|
||||||
'x-team-Id': team.id.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider session={session}>
|
||||||
<LimitsProvider teamId={team.id}>
|
<LimitsProvider teamId={team.id}>
|
||||||
@ -66,9 +61,7 @@ export default async function AuthenticatedTeamsLayout({
|
|||||||
<Header user={user} teams={teams} />
|
<Header user={user} teams={teams} />
|
||||||
|
|
||||||
<TeamProvider team={team}>
|
<TeamProvider team={team}>
|
||||||
<TrpcProvider headers={trpcHeaders}>
|
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
||||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
|
|
||||||
</TrpcProvider>
|
|
||||||
</TeamProvider>
|
</TeamProvider>
|
||||||
|
|
||||||
<RefreshOnFocus />
|
<RefreshOnFocus />
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId,
|
templateId,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
type: TemplateType.PRIVATE,
|
type: TemplateType.PRIVATE,
|
||||||
},
|
},
|
||||||
@ -157,6 +158,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await updateTemplateSettings({
|
||||||
templateId: selectedTemplateId,
|
templateId: selectedTemplateId,
|
||||||
|
teamId: team?.id,
|
||||||
data: {
|
data: {
|
||||||
type: TemplateType.PUBLIC,
|
type: TemplateType.PUBLIC,
|
||||||
publicTitle,
|
publicTitle,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const logger = buildLogger();
|
|||||||
export default createOpenApiNextHandler<typeof appRouter>({
|
export default createOpenApiNextHandler<typeof appRouter>({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
|
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
|
||||||
createTrpcContext({ req, res, requestSource: 'apiV2' }),
|
createTrpcContext({ req, res }),
|
||||||
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
|
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
|
||||||
// Always log the error for now.
|
// Always log the error for now.
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
@ -17,7 +17,7 @@ const logger = buildLogger();
|
|||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: async ({ req, res }) => createTrpcContext({ req, res, requestSource: 'app' }),
|
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
|
||||||
onError(opts) {
|
onError(opts) {
|
||||||
const { error, path } = opts;
|
const { error, path } = opts;
|
||||||
|
|
||||||
|
|||||||
15235
package-lock.json
generated
15235
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.0-rc.7",
|
"version": "1.9.0-rc.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
"dev": "turbo run dev --filter=@documenso/web",
|
"dev": "turbo run dev --filter=@documenso/web",
|
||||||
|
"dev:remix": "turbo run dev --filter=@documenso/remix",
|
||||||
"dev:web": "turbo run dev --filter=@documenso/web",
|
"dev:web": "turbo run dev --filter=@documenso/web",
|
||||||
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
||||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||||
@ -69,14 +70,14 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
"next": "14.2.23",
|
|
||||||
"next-runtime-env": "^3.2.0",
|
"next-runtime-env": "^3.2.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"typescript": "5.7.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.1",
|
"next": "14.2.6",
|
||||||
"next": "14.2.23"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"@ts-rest/next": "^3.30.5",
|
"@ts-rest/next": "^3.30.5",
|
||||||
"@ts-rest/open-api": "^3.33.0",
|
"@ts-rest/open-api": "^3.33.0",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"swagger-ui-react": "^5.11.0",
|
"swagger-ui-react": "^5.11.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { createNextRoute } from '@ts-rest/next';
|
import { createNextRoute } from '@ts-rest/next';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
@ -16,7 +15,8 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
|
|||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
|
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
@ -25,7 +25,7 @@ import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-for
|
|||||||
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
||||||
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
|
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||||
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
||||||
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
||||||
@ -36,10 +36,10 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
|
|||||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||||
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDropdownFieldMeta,
|
ZDropdownFieldMeta,
|
||||||
ZFieldMetaSchema,
|
|
||||||
ZNumberFieldMeta,
|
ZNumberFieldMeta,
|
||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
@ -53,7 +53,6 @@ import {
|
|||||||
} from '@documenso/lib/universal/upload/server-actions';
|
} from '@documenso/lib/universal/upload/server-actions';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { Prisma } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
@ -63,7 +62,6 @@ import {
|
|||||||
|
|
||||||
import { ApiContractV1 } from './contract';
|
import { ApiContractV1 } from './contract';
|
||||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||||
import { ZTemplateWithDataSchema } from './schema';
|
|
||||||
|
|
||||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||||
@ -98,14 +96,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
|
|
||||||
const recipients = await getRecipientsForDocument({
|
const recipients = await getRecipientsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = await getFieldsForDocument({
|
const fields = await getFieldsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedMetaFields = fields.map((field) => {
|
const parsedMetaFields = fields.map((field) => {
|
||||||
@ -210,7 +207,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -233,7 +230,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
id: document.id,
|
id: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
requestMetadata: metadata,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -250,7 +246,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body } = args;
|
const { body } = args;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -318,13 +314,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
formValues: body.formValues,
|
formValues: body.formValues,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
subject: body.meta.subject,
|
subject: body.meta.subject,
|
||||||
message: body.meta.message,
|
message: body.meta.message,
|
||||||
timezone,
|
timezone,
|
||||||
@ -335,7 +330,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||||
distributionMethod: body.meta.distributionMethod,
|
distributionMethod: body.meta.distributionMethod,
|
||||||
emailSettings: body.meta.emailSettings,
|
emailSettings: body.meta.emailSettings,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (body.authOptions) {
|
if (body.authOptions) {
|
||||||
@ -346,16 +341,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
data: {
|
data: {
|
||||||
...body.authOptions,
|
...body.authOptions,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recipients } = await setDocumentRecipients({
|
const { recipients } = await setRecipientsForDocument({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipients: body.recipients,
|
recipients: body.recipients,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -419,11 +414,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = ZTemplateWithDataSchema.parse(template);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: parsed,
|
body: template,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return AppError.toRestAPIError(err);
|
return AppError.toRestAPIError(err);
|
||||||
@ -442,12 +435,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = z.array(ZTemplateWithDataSchema).parse(templates);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
templates: parsed,
|
templates,
|
||||||
totalPages,
|
totalPages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -456,7 +447,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body, params } = args;
|
const { body, params } = args;
|
||||||
|
|
||||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||||
@ -520,9 +511,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
...body.meta,
|
...body.meta,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,7 +522,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
data: body.authOptions,
|
data: body.authOptions,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,7 +544,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { body, params } = args;
|
const { body, params } = args;
|
||||||
|
|
||||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||||
@ -583,7 +573,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
title: body.title,
|
title: body.title,
|
||||||
...body.meta,
|
...body.meta,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return AppError.toRestAPIError(err);
|
return AppError.toRestAPIError(err);
|
||||||
@ -626,7 +615,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
data: body.authOptions,
|
data: body.authOptions,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +636,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { sendEmail, sendCompletionEmails } = args.body;
|
const { sendEmail, sendCompletionEmails } = args.body;
|
||||||
|
|
||||||
@ -683,13 +672,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
|
||||||
emailSettings: {
|
emailSettings: {
|
||||||
...emailSettings,
|
...emailSettings,
|
||||||
documentCompleted: sendCompletionEmails,
|
documentCompleted: sendCompletionEmails,
|
||||||
ownerDocumentCompleted: sendCompletionEmails,
|
ownerDocumentCompleted: sendCompletionEmails,
|
||||||
},
|
},
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,7 +686,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
sendEmail,
|
sendEmail,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -722,7 +710,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
resendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { recipients } = args.body;
|
const { recipients } = args.body;
|
||||||
|
|
||||||
@ -732,7 +720,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
recipients,
|
recipients,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -751,7 +739,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||||
|
|
||||||
@ -797,7 +785,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { recipients: newRecipients } = await setDocumentRecipients({
|
const { recipients: newRecipients } = await setRecipientsForDocument({
|
||||||
documentId: Number(documentId),
|
documentId: Number(documentId),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
@ -815,7 +803,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
actionAuth: authOptions?.actionAuth ?? null,
|
actionAuth: authOptions?.actionAuth ?? null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requestMetadata: metadata,
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||||
@ -1580,39 +1568,3 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateDocument = async ({
|
|
||||||
documentId,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
documentId: number;
|
|
||||||
data: Prisma.DocumentUpdateInput;
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
}) => {
|
|
||||||
return await prisma.document.update({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import type { NextApiRequest } from 'next';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import type { Team, User } from '@documenso/prisma/client';
|
import type { Team, User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const authenticatedMiddleware = <
|
export const authenticatedMiddleware = <
|
||||||
@ -15,12 +13,7 @@ export const authenticatedMiddleware = <
|
|||||||
body: unknown;
|
body: unknown;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
handler: (
|
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
|
||||||
args: T,
|
|
||||||
user: User,
|
|
||||||
team: Team | null | undefined,
|
|
||||||
options: { metadata: ApiRequestMetadata },
|
|
||||||
) => Promise<R>,
|
|
||||||
) => {
|
) => {
|
||||||
return async (args: T) => {
|
return async (args: T) => {
|
||||||
try {
|
try {
|
||||||
@ -43,18 +36,7 @@ export const authenticatedMiddleware = <
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata: ApiRequestMetadata = {
|
return await handler(args, apiToken.user, apiToken.team);
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
||||||
source: 'apiV1',
|
|
||||||
auth: 'api',
|
|
||||||
auditUser: {
|
|
||||||
id: apiToken.team ? null : apiToken.user.id,
|
|
||||||
email: apiToken.team ? null : apiToken.user.email,
|
|
||||||
name: apiToken.team?.name ?? apiToken.user.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return await handler(args, apiToken.user, apiToken.team, { metadata });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log({ err: err });
|
console.log({ err: err });
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
|||||||
fields: z.lazy(() =>
|
fields: z.lazy(() =>
|
||||||
ZFieldSchema.pick({
|
ZFieldSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
documentId: true,
|
|
||||||
recipientId: true,
|
recipientId: true,
|
||||||
type: true,
|
type: true,
|
||||||
page: true,
|
page: true,
|
||||||
@ -69,8 +68,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
|
|||||||
positionY: true,
|
positionY: true,
|
||||||
width: true,
|
width: true,
|
||||||
height: true,
|
height: true,
|
||||||
customText: true,
|
|
||||||
fieldMeta: true,
|
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
fieldMeta: ZFieldMetaSchema.nullish(),
|
fieldMeta: ZFieldMetaSchema.nullish(),
|
||||||
@ -527,7 +524,6 @@ export const ZFieldSchema = z.object({
|
|||||||
height: z.unknown(),
|
height: z.unknown(),
|
||||||
customText: z.string(),
|
customText: z.string(),
|
||||||
inserted: z.boolean(),
|
inserted: z.boolean(),
|
||||||
fieldMeta: ZFieldMetaSchema.nullish().openapi({}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||||
@ -545,8 +541,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
|||||||
}),
|
}),
|
||||||
Field: ZFieldSchema.pick({
|
Field: ZFieldSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
documentId: true,
|
|
||||||
templateId: true,
|
|
||||||
recipientId: true,
|
recipientId: true,
|
||||||
type: true,
|
type: true,
|
||||||
page: true,
|
page: true,
|
||||||
@ -554,8 +548,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
|||||||
positionY: true,
|
positionY: true,
|
||||||
width: true,
|
width: true,
|
||||||
height: true,
|
height: true,
|
||||||
customText: true,
|
|
||||||
fieldMeta: true,
|
|
||||||
}).array(),
|
}).array(),
|
||||||
Recipient: ZRecipientSchema.pick({
|
Recipient: ZRecipientSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
@ -159,109 +157,3 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
|||||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set document visibility.
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Navigate back to the edit page to check that the settings are saved correctly.
|
|
||||||
await page.goto(`/t/${team.url}/templates/${template.id}/edit`);
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
|
|
||||||
const team = await seedTeam({
|
|
||||||
createTeamMembers: 2, // Create an additional member to test different roles
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.teamMember.update({
|
|
||||||
where: {
|
|
||||||
id: team.members[1].id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: TeamMemberRole.MANAGER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const owner = team.owner;
|
|
||||||
const managerUser = team.members[1].user;
|
|
||||||
const memberUser = team.members[2].user;
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test as manager
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: managerUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manager should be able to set visibility to managers and above
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
await expect(page.getByText('Admins only')).toBeDisabled();
|
|
||||||
|
|
||||||
// Save and verify
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Test as regular member
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: memberUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Regular member should not be able to modify visibility when set to managers and above
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
|
||||||
|
|
||||||
// Create a new template with 'everyone' visibility
|
|
||||||
const everyoneTemplate = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
visibility: 'EVERYONE',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to the new template
|
|
||||||
await page.goto(`/t/${team.url}/templates/${everyoneTemplate.id}/edit`);
|
|
||||||
|
|
||||||
// Regular member should be able to see but not modify visibility
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Everyone');
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import fs from 'fs';
|
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
@ -17,20 +13,6 @@ test.describe.configure({ mode: 'parallel' });
|
|||||||
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||||
|
|
||||||
// Create a temporary PDF file for testing
|
|
||||||
function createTempPdfFile() {
|
|
||||||
const tempDir = os.tmpdir();
|
|
||||||
const tempFilePath = path.join(tempDir, 'test.pdf');
|
|
||||||
|
|
||||||
// Create a simple PDF file with some content
|
|
||||||
const pdfContent = Buffer.from(
|
|
||||||
'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF',
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(tempFilePath, pdfContent);
|
|
||||||
return tempFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Create a template with all settings filled out
|
* 1. Create a template with all settings filled out
|
||||||
* 2. Create a document from the template
|
* 2. Create a document from the template
|
||||||
@ -301,318 +283,3 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that we can create a document from a template using a custom document
|
|
||||||
* instead of the template's default document.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a document from a template with custom document', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
// Create a temporary PDF file for upload
|
|
||||||
const testPdfPath = createTempPdfFile();
|
|
||||||
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template with custom document
|
|
||||||
await page.waitForURL('/templates');
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Enable custom document upload and upload file
|
|
||||||
await page.getByLabel('Upload custom document').check();
|
|
||||||
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
|
||||||
|
|
||||||
// Wait for upload to complete
|
|
||||||
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
|
||||||
|
|
||||||
// Create document with custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
|
||||||
expect(document.documentData.data).toEqual(pdfContent);
|
|
||||||
expect(document.documentData.initialData).toEqual(pdfContent);
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary file
|
|
||||||
fs.unlinkSync(testPdfPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that we can create a team document from a template using a custom document
|
|
||||||
* instead of the template's default document.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a team document from a template with custom document', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a temporary PDF file for upload
|
|
||||||
const testPdfPath = createTempPdfFile();
|
|
||||||
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template with custom document
|
|
||||||
await page.waitForURL(`/t/${team.url}/templates`);
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Enable custom document upload and upload file
|
|
||||||
await page.getByLabel('Upload custom document').check();
|
|
||||||
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
|
||||||
|
|
||||||
// Wait for upload to complete
|
|
||||||
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
|
||||||
|
|
||||||
// Create document with custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.teamId).toEqual(team.id);
|
|
||||||
expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
|
||||||
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
|
||||||
expect(document.documentData.data).toEqual(pdfContent);
|
|
||||||
expect(document.documentData.initialData).toEqual(pdfContent);
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary file
|
|
||||||
fs.unlinkSync(testPdfPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test verifies that when custom document upload is not enabled,
|
|
||||||
* the document uses the template's original document data.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a document from a template using template document when custom document is not enabled', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_ORIGINAL_DOC');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template without custom document
|
|
||||||
await page.waitForURL('/templates');
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
|
|
||||||
// Verify custom document upload is not checked by default
|
|
||||||
await expect(page.getByLabel('Upload custom document')).not.toBeChecked();
|
|
||||||
|
|
||||||
// Create document without custom document data
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the template's document data
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const templateWithData = await prisma.template.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: template.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
templateDocumentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
|
|
||||||
expect(document.documentData.data).toEqual(templateWithData.templateDocumentData.data);
|
|
||||||
expect(document.documentData.initialData).toEqual(
|
|
||||||
templateWithData.templateDocumentData.initialData,
|
|
||||||
);
|
|
||||||
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE]: should persist document visibility when creating from template', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title and visibility
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_WITH_VISIBILITY');
|
|
||||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
|
||||||
await page.getByLabel('Managers and above').click();
|
|
||||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
|
|
||||||
'Managers and above',
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add a signer
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Test creating document as team manager
|
|
||||||
await prisma.teamMember.update({
|
|
||||||
where: {
|
|
||||||
id: team.members[1].id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: TeamMemberRole.MANAGER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const managerUser = team.members[1].user;
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: managerUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the correct visibility
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_WITH_VISIBILITY');
|
|
||||||
expect(document.visibility).toEqual('MANAGER_AND_ABOVE');
|
|
||||||
expect(document.teamId).toEqual(team.id);
|
|
||||||
|
|
||||||
// Test that regular member cannot create document from restricted template
|
|
||||||
const memberUser = team.members[2].user;
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: memberUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Template should not be visible to regular member
|
|
||||||
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|||||||
@ -67,8 +67,6 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) =>
|
|||||||
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
|
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
|
||||||
await page.getByRole('button', { name: 'Create one automatically' }).click();
|
await page.getByRole('button', { name: 'Create one automatically' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
await page.getByTestId('btn-dialog-close').click();
|
await page.getByTestId('btn-dialog-close').click();
|
||||||
|
|
||||||
// Expect badge to appear.
|
// Expect badge to appear.
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.18.1",
|
"@playwright/test": "^1.18.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.8.2",
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@documenso/web": "*",
|
"@documenso/web": "*",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
This file lists all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
This file list all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||||
Copyright (c) 2023 Documenso, Inc
|
Copyright (c) 2023 Documenso, Inc
|
||||||
|
|
||||||
- The Stripe Billing Module
|
- The Stripe Billing Module
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
@ -15,6 +15,6 @@
|
|||||||
"eslint-plugin-package-json": "^0.10.4",
|
"eslint-plugin-package-json": "^0.10.4",
|
||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"eslint-plugin-unused-imports": "^3.1.0",
|
"eslint-plugin-unused-imports": "^3.1.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,10 +42,10 @@
|
|||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"inngest": "^3.19.13",
|
"inngest": "^3.19.13",
|
||||||
"kysely": "^0.26.3",
|
"kysely": "^0.26.3",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"oslo": "^0.17.0",
|
"oslo": "^0.17.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
diffDocumentMetaChanges,
|
diffDocumentMetaChanges,
|
||||||
@ -13,8 +13,6 @@ import type { SupportedLanguageCodes } from '../../constants/i18n';
|
|||||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
|
|
||||||
export type CreateDocumentMetaOptions = {
|
export type CreateDocumentMetaOptions = {
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@ -27,18 +25,18 @@ export type CreateDocumentMetaOptions = {
|
|||||||
distributionMethod?: DocumentDistributionMethod;
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
requestMetadata: ApiRequestMetadata;
|
userId: number;
|
||||||
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDocumentMeta = async ({
|
export const upsertDocumentMeta = async ({
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
timezone,
|
timezone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
documentId,
|
documentId,
|
||||||
password,
|
password,
|
||||||
|
userId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
emailSettings,
|
emailSettings,
|
||||||
@ -47,24 +45,34 @@ export const upsertDocumentMeta = async ({
|
|||||||
language,
|
language,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId: user.id,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
@ -112,7 +120,8 @@ export const upsertDocumentMeta = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||||
documentId,
|
documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { z } from 'zod';
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
@ -27,7 +27,7 @@ export type CreateDocumentOptions = {
|
|||||||
formValues?: Record<string, string | number | boolean>;
|
formValues?: Record<string, string | number | boolean>;
|
||||||
normalizePdf?: boolean;
|
normalizePdf?: boolean;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
||||||
@ -162,7 +162,8 @@ export const createDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
source: {
|
source: {
|
||||||
|
|||||||
@ -20,10 +20,9 @@ import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
|||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
@ -32,7 +31,7 @@ export type DeleteDocumentOptions = {
|
|||||||
id: number;
|
id: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteDocument = async ({
|
export const deleteDocument = async ({
|
||||||
@ -48,9 +47,7 @@ export const deleteDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new Error('User not found');
|
||||||
message: 'User not found',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
@ -70,9 +67,7 @@ export const deleteDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new Error('Document not found');
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserOwner = document.userId === userId;
|
const isUserOwner = document.userId === userId;
|
||||||
@ -80,9 +75,7 @@ export const deleteDocument = async ({
|
|||||||
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
||||||
|
|
||||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
throw new Error('Not allowed');
|
||||||
message: 'Not allowed',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hard or soft deleting the actual document if user has permission.
|
// Handle hard or soft deleting the actual document if user has permission.
|
||||||
@ -137,7 +130,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
|||||||
})
|
})
|
||||||
| null;
|
| null;
|
||||||
user: User;
|
user: User;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDocumentOwnerDelete = async ({
|
const handleDocumentOwnerDelete = async ({
|
||||||
@ -157,7 +150,8 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
type: 'SOFT',
|
type: 'SOFT',
|
||||||
},
|
},
|
||||||
@ -183,7 +177,8 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
type: 'HARD',
|
type: 'HARD',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|||||||
|
|
||||||
export interface FindDocumentAuditLogsOptions {
|
export interface FindDocumentAuditLogsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
@ -22,7 +21,6 @@ export interface FindDocumentAuditLogsOptions {
|
|||||||
|
|
||||||
export const findDocumentAuditLogs = async ({
|
export const findDocumentAuditLogs = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
documentId,
|
documentId,
|
||||||
page = 1,
|
page = 1,
|
||||||
perPage = 30,
|
perPage = 30,
|
||||||
@ -36,21 +34,20 @@ export const findDocumentAuditLogs = async ({
|
|||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ export type MoveDocumentToTeamOptions = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
||||||
@ -26,6 +26,10 @@ export const moveDocumentToTeam = async ({
|
|||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const user = await tx.user.findUniqueOrThrow({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
const document = await tx.document.findFirst({
|
const document = await tx.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -35,7 +39,8 @@ export const moveDocumentToTeam = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
message: 'Document not found or already associated with a team.',
|
message: 'Document not found or already associated with a team.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -52,8 +57,9 @@ export const moveDocumentToTeam = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
throw new TRPCError({
|
||||||
message: 'This team does not exist, or you are not a member of this team.',
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You are not a member of this team.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +68,12 @@ export const moveDocumentToTeam = async ({
|
|||||||
data: { teamId },
|
data: { teamId },
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
const log = await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||||
documentId: updatedDocument.id,
|
documentId: updatedDocument.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
movedByUserId: userId,
|
movedByUserId: userId,
|
||||||
fromPersonalAccount: true,
|
fromPersonalAccount: true,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
} from '@documenso/lib/constants/recipient-roles';
|
} from '@documenso/lib/constants/recipient-roles';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
recipients: number[];
|
recipients: number[];
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resendDocument = async ({
|
export const resendDocument = async ({
|
||||||
@ -201,7 +201,8 @@ export const resendDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
emailType: recipientEmailType,
|
emailType: recipientEmailType,
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
@ -31,7 +31,7 @@ export type SendDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
sendEmail?: boolean;
|
sendEmail?: boolean;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
||||||
@ -48,6 +48,17 @@ export const sendDocument = async ({
|
|||||||
sendEmail,
|
sendEmail,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
|
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -187,7 +198,7 @@ export const sendDocument = async ({
|
|||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
requestMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@ -204,7 +215,7 @@ export const sendDocument = async ({
|
|||||||
name: 'internal.seal-document',
|
name: 'internal.seal-document',
|
||||||
payload: {
|
payload: {
|
||||||
documentId,
|
documentId,
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
requestMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,7 +237,8 @@ export const sendDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
metadata: requestMetadata,
|
requestMetadata,
|
||||||
|
user,
|
||||||
data: {},
|
data: {},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
281
packages/lib/server-only/document/update-document-settings.ts
Normal file
281
packages/lib/server-only/document/update-document-settings.ts
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||||
|
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||||
|
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
|
|
||||||
|
export type UpdateDocumentSettingsOptions = {
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
documentId: number;
|
||||||
|
data: {
|
||||||
|
title?: string;
|
||||||
|
externalId?: string | null;
|
||||||
|
visibility?: DocumentVisibility | null;
|
||||||
|
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||||
|
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||||
|
};
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema;
|
||||||
|
|
||||||
|
export type TUpdateDocumentSettingsResponse = z.infer<typeof ZUpdateDocumentSettingsResponseSchema>;
|
||||||
|
|
||||||
|
export const updateDocumentSettings = async ({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
documentId,
|
||||||
|
data,
|
||||||
|
requestMetadata,
|
||||||
|
}: UpdateDocumentSettingsOptions): Promise<TUpdateDocumentSettingsResponse> => {
|
||||||
|
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Missing data to update',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
...(teamId
|
||||||
|
? {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userId,
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const currentUserRole = document.team?.members[0]?.role;
|
||||||
|
const isDocumentOwner = document.userId === userId;
|
||||||
|
const requestedVisibility = data.visibility;
|
||||||
|
|
||||||
|
if (!isDocumentOwner) {
|
||||||
|
match(currentUserRole)
|
||||||
|
.with(TeamMemberRole.ADMIN, () => true)
|
||||||
|
.with(TeamMemberRole.MANAGER, () => {
|
||||||
|
const allowedVisibilities: DocumentVisibility[] = [
|
||||||
|
DocumentVisibility.EVERYONE,
|
||||||
|
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowedVisibilities.includes(document.visibility) ||
|
||||||
|
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document visibility',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with(TeamMemberRole.MEMBER, () => {
|
||||||
|
if (
|
||||||
|
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||||
|
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document visibility',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.otherwise(() => {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to update the document',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: document.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||||
|
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||||
|
|
||||||
|
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||||
|
const newGlobalAccessAuth =
|
||||||
|
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||||
|
const newGlobalActionAuth =
|
||||||
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
|
// Check if user has permission to set the global action auth.
|
||||||
|
if (newGlobalActionAuth) {
|
||||||
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDocumentEnterprise) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'You do not have permission to set the action auth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||||
|
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||||
|
const isGlobalAccessSame =
|
||||||
|
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||||
|
const isGlobalActionSame =
|
||||||
|
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||||
|
const isDocumentVisibilitySame =
|
||||||
|
data.visibility === undefined || data.visibility === document.visibility;
|
||||||
|
|
||||||
|
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||||
|
|
||||||
|
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'You cannot update the title if the document has been sent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTitleSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.title,
|
||||||
|
to: data.title || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExternalIdSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.externalId,
|
||||||
|
to: data.externalId || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalAccessSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: documentGlobalAccessAuth,
|
||||||
|
to: newGlobalAccessAuth,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalActionSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: documentGlobalActionAuth,
|
||||||
|
to: newGlobalActionAuth,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDocumentVisibilitySame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||||
|
documentId,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: document.visibility,
|
||||||
|
to: data.visibility || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return if nothing is required.
|
||||||
|
if (auditLogs.length === 0) {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const authOptions = createDocumentAuthOptions({
|
||||||
|
globalAccessAuth: newGlobalAccessAuth,
|
||||||
|
globalActionAuth: newGlobalActionAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedDocument = await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
externalId: data.externalId,
|
||||||
|
visibility: data.visibility as DocumentVisibility,
|
||||||
|
authOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.createMany({
|
||||||
|
data: auditLogs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedDocument;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,46 +1,23 @@
|
|||||||
import { match } from 'ts-pattern';
|
'use server';
|
||||||
import type { z } from 'zod';
|
|
||||||
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
|
||||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
|
||||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
||||||
|
|
||||||
export type UpdateDocumentOptions = {
|
export type UpdateDocumentOptions = {
|
||||||
|
documentId: number;
|
||||||
|
data: Prisma.DocumentUpdateInput;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
documentId: number;
|
|
||||||
data?: {
|
|
||||||
title?: string;
|
|
||||||
externalId?: string | null;
|
|
||||||
visibility?: DocumentVisibility | null;
|
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
|
||||||
};
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZUpdateDocumentResponseSchema = DocumentSchema;
|
|
||||||
|
|
||||||
export type TUpdateDocumentResponse = z.infer<typeof ZUpdateDocumentResponseSchema>;
|
|
||||||
|
|
||||||
export const updateDocument = async ({
|
export const updateDocument = async ({
|
||||||
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
|
||||||
data,
|
data,
|
||||||
requestMetadata,
|
}: UpdateDocumentOptions) => {
|
||||||
}: UpdateDocumentOptions): Promise<TUpdateDocumentResponse> => {
|
return await prisma.document.update({
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
...(teamId
|
||||||
@ -59,215 +36,8 @@ export const updateDocument = async ({
|
|||||||
teamId: null,
|
teamId: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
include: {
|
data: {
|
||||||
team: {
|
...data,
|
||||||
select: {
|
|
||||||
members: {
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (teamId) {
|
|
||||||
const currentUserRole = document.team?.members[0]?.role;
|
|
||||||
const isDocumentOwner = document.userId === userId;
|
|
||||||
const requestedVisibility = data?.visibility;
|
|
||||||
|
|
||||||
if (!isDocumentOwner) {
|
|
||||||
match(currentUserRole)
|
|
||||||
.with(TeamMemberRole.ADMIN, () => true)
|
|
||||||
.with(TeamMemberRole.MANAGER, () => {
|
|
||||||
const allowedVisibilities: DocumentVisibility[] = [
|
|
||||||
DocumentVisibility.EVERYONE,
|
|
||||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!allowedVisibilities.includes(document.visibility) ||
|
|
||||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document visibility',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with(TeamMemberRole.MEMBER, () => {
|
|
||||||
if (
|
|
||||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
|
||||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document visibility',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.otherwise(() => {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to update the document',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no data just return the document since this function is normally chained after a meta update.
|
|
||||||
if (!data || Object.values(data).length === 0) {
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
|
||||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
|
||||||
|
|
||||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
|
||||||
const newGlobalAccessAuth =
|
|
||||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
|
||||||
const newGlobalActionAuth =
|
|
||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (newGlobalActionAuth) {
|
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDocumentEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
|
||||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
|
||||||
const isGlobalAccessSame =
|
|
||||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
|
||||||
const isGlobalActionSame =
|
|
||||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
|
||||||
const isDocumentVisibilitySame =
|
|
||||||
data.visibility === undefined || data.visibility === document.visibility;
|
|
||||||
|
|
||||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
|
||||||
|
|
||||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
||||||
message: 'You cannot update the title if the document has been sent',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTitleSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.title,
|
|
||||||
to: data.title || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isExternalIdSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.externalId,
|
|
||||||
to: data.externalId || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGlobalAccessSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: documentGlobalAccessAuth,
|
|
||||||
to: newGlobalAccessAuth,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGlobalActionSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: documentGlobalActionAuth,
|
|
||||||
to: newGlobalActionAuth,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDocumentVisibilitySame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: document.visibility,
|
|
||||||
to: data.visibility || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return if nothing is required.
|
|
||||||
if (auditLogs.length === 0) {
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
|
||||||
const authOptions = createDocumentAuthOptions({
|
|
||||||
globalAccessAuth: newGlobalAccessAuth,
|
|
||||||
globalActionAuth: newGlobalActionAuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedDocument = await tx.document.update({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
externalId: data.externalId,
|
|
||||||
visibility: data.visibility as DocumentVisibility,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.createMany({
|
|
||||||
data: auditLogs,
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedDocument;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface CreateDocumentFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
fields: {
|
|
||||||
recipientId: number;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateDocumentFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateDocumentFieldsResponse = z.infer<typeof ZCreateDocumentFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const createDocumentFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
requestMetadata,
|
|
||||||
}: CreateDocumentFieldsOptions): Promise<TCreateDocumentFieldsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field validation.
|
|
||||||
const validatedFields = fields.map((field) => {
|
|
||||||
const recipient = document.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient ${field.recipientId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, document.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
validatedFields.map(async (field) => {
|
|
||||||
const createdField = await tx.field.create({
|
|
||||||
data: {
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.width,
|
|
||||||
height: field.height,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: field.fieldMeta,
|
|
||||||
documentId,
|
|
||||||
recipientId: field.recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle field created audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: createdField.secondaryId,
|
|
||||||
fieldRecipientEmail: field.recipientEmail,
|
|
||||||
fieldRecipientId: createdField.recipientId,
|
|
||||||
fieldType: createdField.type,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: createdFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface CreateTemplateFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
fields: {
|
|
||||||
recipientId: number;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateTemplateFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateTemplateFieldsResponse = z.infer<typeof ZCreateTemplateFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const createTemplateFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
fields,
|
|
||||||
}: CreateTemplateFieldsOptions): Promise<TCreateTemplateFieldsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field validation.
|
|
||||||
const validatedFields = fields.map((field) => {
|
|
||||||
const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient ${field.recipientId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, template.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Recipient type cannot have fields, or they have already interacted with the template.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
validatedFields.map(async (field) => {
|
|
||||||
const createdField = await tx.field.create({
|
|
||||||
data: {
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: field.pageX,
|
|
||||||
positionY: field.pageY,
|
|
||||||
width: field.width,
|
|
||||||
height: field.height,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: field.fieldMeta,
|
|
||||||
templateId,
|
|
||||||
recipientId: field.recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: createdFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface DeleteDocumentFieldOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
fieldId: number;
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteDocumentField = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
fieldId,
|
|
||||||
requestMetadata,
|
|
||||||
}: DeleteDocumentFieldOptions): Promise<void> => {
|
|
||||||
const field = await prisma.field.findFirst({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!field) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentId = field.documentId;
|
|
||||||
|
|
||||||
if (!documentId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field does not belong to a document. Use delete template field instead.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: field.recipientId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.Recipient.find((recipient) => recipient.id === field.recipientId);
|
|
||||||
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient for field ${fieldId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can have new fields created.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, recipient.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Recipient has already interacted with the document.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
const deletedField = await tx.field.delete({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle field deleted audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: deletedField.secondaryId,
|
|
||||||
fieldRecipientEmail: recipient.email,
|
|
||||||
fieldRecipientId: deletedField.recipientId,
|
|
||||||
fieldType: deletedField.type,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface DeleteTemplateFieldOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
fieldId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteTemplateField = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
fieldId,
|
|
||||||
}: DeleteTemplateFieldOptions): Promise<void> => {
|
|
||||||
const field = await prisma.field.findFirst({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
Template: teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!field || !field.templateId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Field not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.field.delete({
|
|
||||||
where: {
|
|
||||||
id: fieldId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -3,34 +3,30 @@ import { prisma } from '@documenso/prisma';
|
|||||||
export interface GetFieldsForDocumentOptions {
|
export interface GetFieldsForDocumentOptions {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||||
|
|
||||||
export const getFieldsForDocument = async ({
|
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
||||||
documentId,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
}: GetFieldsForDocumentOptions) => {
|
|
||||||
const fields = await prisma.field.findMany({
|
const fields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Signature: true,
|
Signature: true,
|
||||||
|
|||||||
35
packages/lib/server-only/field/get-fields-for-template.ts
Normal file
35
packages/lib/server-only/field/get-fields-for-template.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export interface GetFieldsForTemplateOptions {
|
||||||
|
templateId: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForTemplateOptions) => {
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
templateId,
|
||||||
|
Template: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
diffFieldChanges,
|
diffFieldChanges,
|
||||||
@ -31,10 +31,9 @@ import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|||||||
|
|
||||||
export interface SetFieldsForDocumentOptions {
|
export interface SetFieldsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
fields: FieldData[];
|
fields: FieldData[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
||||||
@ -45,7 +44,6 @@ export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocument
|
|||||||
|
|
||||||
export const setFieldsForDocument = async ({
|
export const setFieldsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
documentId,
|
documentId,
|
||||||
fields,
|
fields,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
@ -53,27 +51,37 @@ export const setFieldsForDocument = async ({
|
|||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
message: 'Document not found',
|
message: 'Document not found',
|
||||||
@ -272,7 +280,8 @@ export const setFieldsForDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes,
|
changes,
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
@ -287,7 +296,8 @@ export const setFieldsForDocument = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
},
|
},
|
||||||
@ -315,7 +325,8 @@ export const setFieldsForDocument = async ({
|
|||||||
createDocumentAuditLogData({
|
createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
fieldId: field.secondaryId,
|
fieldId: field.secondaryId,
|
||||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|||||||
|
|
||||||
export type SetFieldsForTemplateOptions = {
|
export type SetFieldsForTemplateOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
templateId: number;
|
||||||
fields: {
|
fields: {
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
@ -43,28 +42,26 @@ export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplate
|
|||||||
|
|
||||||
export const setFieldsForTemplate = async ({
|
export const setFieldsForTemplate = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
templateId,
|
templateId,
|
||||||
fields,
|
fields,
|
||||||
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,165 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import {
|
|
||||||
createDocumentAuditLogData,
|
|
||||||
diffFieldChanges,
|
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateDocumentFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
fields: {
|
|
||||||
id: number;
|
|
||||||
type?: FieldType;
|
|
||||||
pageNumber?: number;
|
|
||||||
pageX?: number;
|
|
||||||
pageY?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateDocumentFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateDocumentFieldsResponse = z.infer<typeof ZUpdateDocumentFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const updateDocumentFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
fields,
|
|
||||||
requestMetadata,
|
|
||||||
}: UpdateDocumentFieldsOptions): Promise<TUpdateDocumentFieldsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsToUpdate = fields.map((field) => {
|
|
||||||
const originalField = document.Field.find((existingField) => existingField.id === field.id);
|
|
||||||
|
|
||||||
if (!originalField) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Field with id ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.Recipient.find(
|
|
||||||
(recipient) => recipient.id === originalField.recipientId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient attached to field ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can be modified.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, document.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Cannot modify a field where the recipient has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalField,
|
|
||||||
updateData: field,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
fieldsToUpdate.map(async ({ originalField, updateData, recipientEmail }) => {
|
|
||||||
const updatedField = await tx.field.update({
|
|
||||||
where: {
|
|
||||||
id: updateData.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: updateData.type,
|
|
||||||
page: updateData.pageNumber,
|
|
||||||
positionX: updateData.pageX,
|
|
||||||
positionY: updateData.pageY,
|
|
||||||
width: updateData.width,
|
|
||||||
height: updateData.height,
|
|
||||||
fieldMeta: updateData.fieldMeta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const changes = diffFieldChanges(originalField, updatedField);
|
|
||||||
|
|
||||||
// Handle field updated audit log.
|
|
||||||
if (changes.length > 0) {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
fieldId: updatedField.secondaryId,
|
|
||||||
fieldRecipientEmail: recipientEmail,
|
|
||||||
fieldRecipientId: updatedField.recipientId,
|
|
||||||
fieldType: updatedField.type,
|
|
||||||
changes,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: updatedFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateTemplateFieldsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
fields: {
|
|
||||||
id: number;
|
|
||||||
type?: FieldType;
|
|
||||||
pageNumber?: number;
|
|
||||||
pageX?: number;
|
|
||||||
pageY?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
fieldMeta?: TFieldMetaSchema;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateTemplateFieldsResponseSchema = z.object({
|
|
||||||
fields: z.array(FieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateTemplateFieldsResponse = z.infer<typeof ZUpdateTemplateFieldsResponseSchema>;
|
|
||||||
|
|
||||||
export const updateTemplateFields = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
fields,
|
|
||||||
}: UpdateTemplateFieldsOptions): Promise<TUpdateTemplateFieldsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsToUpdate = fields.map((field) => {
|
|
||||||
const originalField = template.Field.find((existingField) => existingField.id === field.id);
|
|
||||||
|
|
||||||
if (!originalField) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Field with id ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = template.Recipient.find(
|
|
||||||
(recipient) => recipient.id === originalField.recipientId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
|
||||||
if (!recipient) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Recipient attached to field ${field.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the recipient associated with the field can be modified.
|
|
||||||
if (!canRecipientFieldsBeModified(recipient, template.Field)) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message:
|
|
||||||
'Cannot modify a field where the recipient has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateData: field,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
fieldsToUpdate.map(async ({ updateData }) => {
|
|
||||||
const updatedField = await tx.field.update({
|
|
||||||
where: {
|
|
||||||
id: updateData.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: updateData.type,
|
|
||||||
page: updateData.pageNumber,
|
|
||||||
positionX: updateData.pageX,
|
|
||||||
positionY: updateData.pageY,
|
|
||||||
width: updateData.width,
|
|
||||||
height: updateData.height,
|
|
||||||
fieldMeta: updateData.fieldMeta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedField;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: updatedFields,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -3,13 +3,13 @@ import sharp from 'sharp';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
|
|
||||||
export type SetAvatarImageOptions = {
|
export type SetAvatarImageOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number | null;
|
teamId?: number | null;
|
||||||
bytes?: string | null;
|
bytes?: string | null;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAvatarImage = async ({
|
export const setAvatarImage = async ({
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface CreateDocumentRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
recipients: {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
}[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientBaseResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateDocumentRecipientsResponse = z.infer<
|
|
||||||
typeof ZCreateDocumentRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createDocumentRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
recipients: recipientsToCreate,
|
|
||||||
requestMetadata,
|
|
||||||
}: CreateDocumentRecipientsOptions): Promise<TCreateDocumentRecipientsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
|
||||||
...recipient,
|
|
||||||
email: recipient.email.toLowerCase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
|
||||||
const existingRecipient = document.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return existingRecipient !== undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateRecipients.length > 0) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
normalizedRecipients.map(async (recipient) => {
|
|
||||||
const authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: recipient.accessAuth || null,
|
|
||||||
actionAuth: recipient.actionAuth || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
|
||||||
data: {
|
|
||||||
documentId,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
role: recipient.role,
|
|
||||||
signingOrder: recipient.signingOrder,
|
|
||||||
token: nanoid(),
|
|
||||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle recipient created audit log.
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: createdRecipient.email,
|
|
||||||
recipientName: createdRecipient.name,
|
|
||||||
recipientId: createdRecipient.id,
|
|
||||||
recipientRole: createdRecipient.role,
|
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: createdRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface CreateTemplateRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
templateId: number;
|
|
||||||
recipients: {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientBaseResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateTemplateRecipientsResponse = z.infer<
|
|
||||||
typeof ZCreateTemplateRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createTemplateRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
templateId,
|
|
||||||
recipients: recipientsToCreate,
|
|
||||||
}: CreateTemplateRecipientsOptions): Promise<TCreateTemplateRecipientsResponse> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
id: templateId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
|
||||||
...recipient,
|
|
||||||
email: recipient.email.toLowerCase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
|
||||||
const existingRecipient = template.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return existingRecipient !== undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateRecipients.length > 0) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
normalizedRecipients.map(async (recipient) => {
|
|
||||||
const authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: recipient.accessAuth || null,
|
|
||||||
actionAuth: recipient.actionAuth || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
|
||||||
data: {
|
|
||||||
templateId,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
role: recipient.role,
|
|
||||||
signingOrder: recipient.signingOrder,
|
|
||||||
token: nanoid(),
|
|
||||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: createdRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { SendStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
|
|
||||||
export interface DeleteDocumentRecipientOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
recipientId: number;
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteDocumentRecipient = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
recipientId,
|
|
||||||
requestMetadata,
|
|
||||||
}: DeleteDocumentRecipientOptions): Promise<void> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
Recipient: {
|
|
||||||
some: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
team: true,
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'User not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientToDelete = document.Recipient[0];
|
|
||||||
|
|
||||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Recipient not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
await tx.recipient.delete({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
|
||||||
documentId: document.id,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: recipientToDelete.email,
|
|
||||||
recipientName: recipientToDelete.name,
|
|
||||||
recipientId: recipientToDelete.id,
|
|
||||||
recipientRole: recipientToDelete.role,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
||||||
document.documentMeta,
|
|
||||||
).recipientRemoved;
|
|
||||||
|
|
||||||
// Send email to deleted recipient.
|
|
||||||
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
|
|
||||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
inviterName: document.team?.name || user.name || undefined,
|
|
||||||
assetBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
|
|
||||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipientToDelete.email,
|
|
||||||
name: recipientToDelete.name,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`You have been removed from a document`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
|
|
||||||
export interface DeleteTemplateRecipientOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
recipientId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteTemplateRecipient = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
recipientId,
|
|
||||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
|
||||||
const template = await prisma.template.findFirst({
|
|
||||||
where: {
|
|
||||||
Recipient: {
|
|
||||||
some: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: {
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Template not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientToDelete = template.Recipient[0];
|
|
||||||
|
|
||||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Recipient not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.recipient.delete({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -29,21 +29,25 @@ export const getRecipientById = async ({
|
|||||||
const recipient = await prisma.recipient.findFirst({
|
const recipient = await prisma.recipient.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: recipientId,
|
id: recipientId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
team: {
|
teamId === undefined
|
||||||
id: teamId,
|
? {
|
||||||
members: {
|
userId,
|
||||||
some: {
|
teamId: null,
|
||||||
userId,
|
}
|
||||||
|
: {
|
||||||
|
teamId,
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
}
|
},
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Field: true,
|
Field: true,
|
||||||
|
|||||||
@ -14,21 +14,23 @@ export const getRecipientsForDocument = async ({
|
|||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
Document: teamId
|
Document: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId,
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -3,32 +3,31 @@ import { prisma } from '@documenso/prisma';
|
|||||||
export interface GetRecipientsForTemplateOptions {
|
export interface GetRecipientsForTemplateOptions {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRecipientsForTemplate = async ({
|
export const getRecipientsForTemplate = async ({
|
||||||
templateId,
|
templateId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
|
||||||
}: GetRecipientsForTemplateOptions) => {
|
}: GetRecipientsForTemplateOptions) => {
|
||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
Template: teamId
|
Template: {
|
||||||
? {
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
team: {
|
team: {
|
||||||
id: teamId,
|
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-ent
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
import {
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
ZRecipientAuthOptionsSchema,
|
ZRecipientAuthOptionsSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import {
|
import {
|
||||||
createDocumentAuditLogData,
|
createDocumentAuditLogData,
|
||||||
@ -34,27 +33,29 @@ import { canRecipientBeModified } from '../../utils/recipients';
|
|||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
|
|
||||||
export interface SetDocumentRecipientsOptions {
|
export interface SetRecipientsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
recipients: RecipientData[];
|
recipients: RecipientData[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZSetDocumentRecipientsResponseSchema = z.object({
|
export const ZSetRecipientsForDocumentResponseSchema = z.object({
|
||||||
recipients: RecipientSchema.array(),
|
recipients: RecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSetDocumentRecipientsResponse = z.infer<typeof ZSetDocumentRecipientsResponseSchema>;
|
export type TSetRecipientsForDocumentResponse = z.infer<
|
||||||
|
typeof ZSetRecipientsForDocumentResponseSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const setDocumentRecipients = async ({
|
export const setRecipientsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
documentId,
|
||||||
recipients,
|
recipients,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SetDocumentRecipientsOptions): Promise<TSetDocumentRecipientsResponse> => {
|
}: SetRecipientsForDocumentOptions): Promise<TSetRecipientsForDocumentResponse> => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -166,10 +167,10 @@ export const setDocumentRecipients = async ({
|
|||||||
linkedRecipients.map(async (recipient) => {
|
linkedRecipients.map(async (recipient) => {
|
||||||
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
|
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
|
||||||
|
|
||||||
if (recipient.actionAuth !== undefined || recipient.accessAuth !== undefined) {
|
if (recipient.actionAuth !== undefined) {
|
||||||
authOptions = createRecipientAuthOptions({
|
authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || authOptions.accessAuth,
|
accessAuth: authOptions.accessAuth,
|
||||||
actionAuth: recipient.actionAuth || authOptions.actionAuth,
|
actionAuth: recipient.actionAuth,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +236,8 @@ export const setDocumentRecipients = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
changes,
|
changes,
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
@ -250,10 +252,10 @@ export const setDocumentRecipients = async ({
|
|||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth || undefined,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -280,7 +282,8 @@ export const setDocumentRecipients = async ({
|
|||||||
createDocumentAuditLogData({
|
createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
metadata: requestMetadata,
|
user,
|
||||||
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
@ -365,22 +368,17 @@ type RecipientData = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
recipient.email !== newRecipientData.email ||
|
recipient.email !== newRecipientData.email ||
|
||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
authOptions.actionAuth !== newRecipientData.actionAuth
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
import { nanoid } from '../../universal/id';
|
import { nanoid } from '../../universal/id';
|
||||||
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
||||||
|
|
||||||
export type SetTemplateRecipientsOptions = {
|
export type SetRecipientsForTemplateOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
templateId: number;
|
templateId: number;
|
||||||
@ -32,36 +32,37 @@ export type SetTemplateRecipientsOptions = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZSetTemplateRecipientsResponseSchema = z.object({
|
export const ZSetRecipientsForTemplateResponseSchema = z.object({
|
||||||
recipients: RecipientSchema.array(),
|
recipients: RecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSetTemplateRecipientsResponse = z.infer<typeof ZSetTemplateRecipientsResponseSchema>;
|
export type TSetRecipientsForTemplateResponse = z.infer<
|
||||||
|
typeof ZSetRecipientsForTemplateResponseSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const setTemplateRecipients = async ({
|
export const setRecipientsForTemplate = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
templateId,
|
templateId,
|
||||||
recipients,
|
recipients,
|
||||||
}: SetTemplateRecipientsOptions): Promise<TSetTemplateRecipientsResponse> => {
|
}: SetRecipientsForTemplateOptions): Promise<TSetRecipientsForTemplateResponse> => {
|
||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
...(teamId
|
OR: [
|
||||||
? {
|
{
|
||||||
team: {
|
userId,
|
||||||
id: teamId,
|
},
|
||||||
members: {
|
{
|
||||||
some: {
|
team: {
|
||||||
userId,
|
members: {
|
||||||
},
|
some: {
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
: {
|
},
|
||||||
userId,
|
],
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
directLink: true,
|
directLink: true,
|
||||||
@ -1,249 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
|
||||||
type TRecipientActionAuthTypes,
|
|
||||||
ZRecipientAuthOptionsSchema,
|
|
||||||
} from '@documenso/lib/types/document-auth';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import {
|
|
||||||
createDocumentAuditLogData,
|
|
||||||
diffRecipientChanges,
|
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { ZRecipientResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
|
||||||
|
|
||||||
export interface UpdateDocumentRecipientsOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
|
||||||
recipients: RecipientData[];
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
|
||||||
recipients: ZRecipientResponseSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUpdateDocumentRecipientsResponse = z.infer<
|
|
||||||
typeof ZUpdateDocumentRecipientsResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const updateDocumentRecipients = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
documentId,
|
|
||||||
recipients,
|
|
||||||
requestMetadata,
|
|
||||||
}: UpdateDocumentRecipientsOptions): Promise<TUpdateDocumentRecipientsResponse> => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
...(teamId
|
|
||||||
? {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.completedAt) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Document already complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
|
||||||
if (recipientsHaveActionAuth) {
|
|
||||||
const isEnterprise = await isUserEnterprise({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isEnterprise) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'You do not have permission to set the action auth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipientsToUpdate = recipients.map((recipient) => {
|
|
||||||
const originalRecipient = document.Recipient.find(
|
|
||||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!originalRecipient) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: `Recipient with id ${recipient.id} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateRecipientWithSameEmail = document.Recipient.find(
|
|
||||||
(existingRecipient) =>
|
|
||||||
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (duplicateRecipientWithSameEmail) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasRecipientBeenChanged(originalRecipient, recipient) &&
|
|
||||||
!canRecipientBeModified(originalRecipient, document.Field)
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalRecipient,
|
|
||||||
updateData: recipient,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedRecipients = await prisma.$transaction(async (tx) => {
|
|
||||||
return await Promise.all(
|
|
||||||
recipientsToUpdate.map(async ({ originalRecipient, updateData }) => {
|
|
||||||
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
|
|
||||||
|
|
||||||
if (updateData.actionAuth !== undefined || updateData.accessAuth !== undefined) {
|
|
||||||
authOptions = createRecipientAuthOptions({
|
|
||||||
accessAuth: updateData.accessAuth || authOptions.accessAuth,
|
|
||||||
actionAuth: updateData.actionAuth || authOptions.actionAuth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedRecipient = {
|
|
||||||
...originalRecipient,
|
|
||||||
...updateData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedRecipient = await tx.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: originalRecipient.id,
|
|
||||||
documentId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: mergedRecipient.name,
|
|
||||||
email: mergedRecipient.email,
|
|
||||||
role: mergedRecipient.role,
|
|
||||||
signingOrder: mergedRecipient.signingOrder,
|
|
||||||
documentId,
|
|
||||||
sendStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
||||||
signingStatus:
|
|
||||||
mergedRecipient.role === RecipientRole.CC
|
|
||||||
? SigningStatus.SIGNED
|
|
||||||
: SigningStatus.NOT_SIGNED,
|
|
||||||
authOptions,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Field: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear all fields if the recipient role is changed to a type that cannot have fields.
|
|
||||||
if (
|
|
||||||
originalRecipient.role !== updatedRecipient.role &&
|
|
||||||
(updatedRecipient.role === RecipientRole.CC ||
|
|
||||||
updatedRecipient.role === RecipientRole.VIEWER)
|
|
||||||
) {
|
|
||||||
await tx.field.deleteMany({
|
|
||||||
where: {
|
|
||||||
recipientId: updatedRecipient.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
|
|
||||||
|
|
||||||
// Handle recipient updated audit log.
|
|
||||||
if (changes.length > 0) {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
|
||||||
documentId: documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
recipientEmail: updatedRecipient.email,
|
|
||||||
recipientName: updatedRecipient.name,
|
|
||||||
recipientId: updatedRecipient.id,
|
|
||||||
recipientRole: updatedRecipient.role,
|
|
||||||
changes,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedRecipient;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipients: updatedRecipients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
|
||||||
*/
|
|
||||||
type RecipientData = {
|
|
||||||
id: number;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
role?: RecipientRole;
|
|
||||||
signingOrder?: number | null;
|
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
recipient.email !== newRecipientData.email ||
|
|
||||||
recipient.name !== newRecipientData.name ||
|
|
||||||
recipient.role !== newRecipientData.role ||
|
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user