diff --git a/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx b/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
index 013c5fa49..3526bdd49 100644
--- a/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
+++ b/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
@@ -59,10 +59,10 @@ export async function loader({ params }: Route.LoaderArgs) {
}
return superLoaderJson({
- isAccessAuthValid: true as const,
+ isAccessAuthValid: true,
template,
directTemplateRecipient,
- });
+ } as const);
}
export default function DirectTemplatePage() {
diff --git a/apps/remix/package.json b/apps/remix/package.json
index 3046295d2..160983a20 100644
--- a/apps/remix/package.json
+++ b/apps/remix/package.json
@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "sh .bin/build.sh",
- "build:app": "cross-env NODE_ENV=production react-router build",
+ "build:app": "npm run typecheck && cross-env NODE_ENV=production react-router build",
"build:server": "cross-env NODE_ENV=production rollup -c rollup.config.mjs",
"dev": "npm run with:env -- react-router dev",
"start": "npm run with:env -- cross-env NODE_ENV=production node build/server/main.js",
@@ -26,7 +26,6 @@
"@epic-web/remember": "^1.1.0",
"@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4",
- "@hono/zod-validator": "^0.4.2",
"@hookform/resolvers": "^3.1.0",
"@lingui/core": "^5.2.0",
"@lingui/detect-locale": "^5.2.0",
@@ -41,7 +40,7 @@
"autoprefixer": "^10.4.13",
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
- "hono": "4.6.15",
+ "hono": "4.7.0",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@@ -98,6 +97,7 @@
"typescript": "5.6.2",
"vite": "^6.1.0",
"vite-plugin-babel-macros": "^1.0.6",
+ "vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.1.4"
}
-}
+}
\ No newline at end of file
diff --git a/apps/remix/rollup.config.mjs b/apps/remix/rollup.config.mjs
index 611cefe50..aa71dba19 100644
--- a/apps/remix/rollup.config.mjs
+++ b/apps/remix/rollup.config.mjs
@@ -1,3 +1,4 @@
+import linguiMacro from '@lingui/babel-plugin-lingui-macro';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
@@ -21,7 +22,7 @@ const config = {
external: [/node_modules/],
plugins: [
typescript({
- // noEmitOnError: true,
+ noEmitOnError: true,
moduleResolution: 'bundler',
include: ['server/**/*', '../../packages/**/*', '../../packages/lib/translations/**/*'],
}),
@@ -33,6 +34,7 @@ const config = {
'@documenso/auth/*',
'@documenso/lib/*',
'@documenso/trpc/*',
+ '@documenso/email/*',
],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
}),
@@ -41,7 +43,7 @@ const config = {
babelHelpers: 'bundled',
extensions: ['.js', '.ts', '.tsx'],
presets: ['@babel/preset-typescript'],
- plugins: ['@lingui/babel-plugin-lingui-macro'],
+ plugins: [linguiMacro],
}),
],
};
diff --git a/apps/remix/server/api/files.ts b/apps/remix/server/api/files.ts
new file mode 100644
index 000000000..8a54f6f33
--- /dev/null
+++ b/apps/remix/server/api/files.ts
@@ -0,0 +1,110 @@
+import { sValidator } from '@hono/standard-validator';
+import { Hono } from 'hono';
+import { PDFDocument } from 'pdf-lib';
+
+import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
+import { putFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
+import {
+ getPresignGetUrl,
+ getPresignPostUrl,
+} from '@documenso/lib/universal/upload/server-actions';
+
+import type { HonoEnv } from '../router';
+import {
+ type TGetPresignedGetUrlResponse,
+ type TGetPresignedPostUrlResponse,
+ ZGetPresignedGetUrlRequestSchema,
+ ZGetPresignedPostUrlRequestSchema,
+ ZUploadPdfRequestSchema,
+} from './files.types';
+
+export const filesRoute = new Hono
()
+ /**
+ * Uploads a document file to the appropriate storage location and creates
+ * a document data record.
+ */
+ .post('/upload-pdf', sValidator('form', ZUploadPdfRequestSchema), async (c) => {
+ try {
+ const { file } = c.req.valid('form');
+
+ if (!file) {
+ return c.json({ error: 'No file provided' }, 400);
+ }
+
+ // Todo: Do we want to validate the file type?
+ // if (file.type !== 'application/pdf') {
+ // return c.json({ error: 'File must be a PDF' }, 400);
+ // }
+
+ // Todo: This is new.
+ // Add file size validation.
+ // Convert MB to bytes (1 MB = 1024 * 1024 bytes)
+ const MAX_FILE_SIZE = APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024;
+
+ if (file.size > MAX_FILE_SIZE) {
+ return c.json({ error: 'File too large' }, 400);
+ }
+
+ const arrayBuffer = await file.arrayBuffer();
+
+ const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
+ console.error(`PDF upload parse error: ${e.message}`);
+
+ throw new AppError('INVALID_DOCUMENT_FILE');
+ });
+
+ if (pdf.isEncrypted) {
+ throw new AppError('INVALID_DOCUMENT_FILE');
+ }
+
+ // Todo: Test this.
+ if (!file.name.endsWith('.pdf')) {
+ Object.defineProperty(file, 'name', {
+ writable: true,
+ value: `${file.name}.pdf`,
+ });
+ }
+
+ const { type, data } = await putFileServerSide(file);
+
+ const result = await createDocumentData({ type, data });
+
+ return c.json(result);
+ } catch (error) {
+ console.error('Upload failed:', error);
+ return c.json({ error: 'Upload failed' }, 500);
+ }
+ })
+ .post('/presigned-get-url', sValidator('json', ZGetPresignedGetUrlRequestSchema), async (c) => {
+ const { key } = await c.req.json();
+ console.log(key);
+
+ try {
+ const { url } = await getPresignGetUrl(key || '');
+
+ return c.json({ url } satisfies TGetPresignedGetUrlResponse);
+ } catch (err) {
+ console.error(err);
+
+ throw new AppError(AppErrorCode.UNKNOWN_ERROR);
+ }
+ })
+ .post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => {
+ const { fileName, contentType } = c.req.valid('json');
+
+ try {
+ console.log({
+ fileName,
+ });
+ const { key, url } = await getPresignPostUrl(fileName, contentType);
+ console.log(key);
+
+ return c.json({ key, url } satisfies TGetPresignedPostUrlResponse);
+ } catch (err) {
+ console.error(err);
+
+ throw new AppError(AppErrorCode.UNKNOWN_ERROR);
+ }
+ });
diff --git a/apps/remix/server/api/files.types.ts b/apps/remix/server/api/files.types.ts
new file mode 100644
index 000000000..8c120cccd
--- /dev/null
+++ b/apps/remix/server/api/files.types.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+
+import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
+
+export const ZUploadPdfRequestSchema = z.object({
+ file: z.instanceof(File),
+});
+
+export const ZUploadPdfResponseSchema = DocumentDataSchema.pick({
+ type: true,
+ id: true,
+});
+
+export type TUploadPdfRequest = z.infer;
+export type TUploadPdfResponse = z.infer;
+
+export const ZGetPresignedPostUrlRequestSchema = z.object({
+ fileName: z.string().min(1),
+ contentType: z.string().min(1),
+});
+
+export const ZGetPresignedPostUrlResponseSchema = z.object({
+ key: z.string().min(1),
+ url: z.string().min(1),
+});
+
+export const ZGetPresignedGetUrlRequestSchema = z.object({
+ key: z.string().min(1),
+});
+
+export const ZGetPresignedGetUrlResponseSchema = z.object({
+ url: z.string().min(1),
+});
+
+export type TGetPresignedPostUrlRequest = z.infer;
+export type TGetPresignedPostUrlResponse = z.infer;
+export type TGetPresignedGetUrlRequest = z.infer;
+export type TGetPresignedGetUrlResponse = z.infer;
diff --git a/apps/remix/server/middleware.ts b/apps/remix/server/middleware.ts
index 3dbc58970..d34d451e9 100644
--- a/apps/remix/server/middleware.ts
+++ b/apps/remix/server/middleware.ts
@@ -1,12 +1,20 @@
import type { Context, Next } from 'hono';
-import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
+import { getCookie } from 'hono/cookie';
-import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
+import { setCsrfCookie } from '@documenso/auth/server/lib/session/session-cookies';
import { AppLogger } from '@documenso/lib/utils/debugger';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
const logger = new AppLogger('Middleware');
+/**
+ * Middleware for initial page loads.
+ *
+ * You won't be able to easily handle sequential page loads because they will be
+ * called under `path.data`
+ *
+ * Example an initial page load would be `/documents` then if the user click templates
+ * the path here would be `/templates.data`.
+ */
export const appMiddleware = async (c: Context, next: Next) => {
const { req } = c;
const { path } = req;
@@ -24,70 +32,64 @@ export const appMiddleware = async (c: Context, next: Next) => {
const referrerUrl = referrer ? new URL(referrer) : null;
const referrerPathname = referrerUrl ? referrerUrl.pathname : null;
- // Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
- const resetPreferredTeamUrl =
- referrerPathname &&
- referrerPathname.startsWith('/t/') &&
- (!path.startsWith('/t/') || path === '/');
+ // Set csrf token if not set.
+ const csrfToken = getCookie(c, 'csrfToken');
- // Redirect root page to `/documents` or `/t/{preferredTeamUrl}/documents`.
- if (path === '/') {
- logger.log('Redirecting from root to documents');
-
- const redirectUrlPath = formatDocumentsPath(
- resetPreferredTeamUrl ? undefined : preferredTeamUrl,
- );
-
- const redirectUrl = new URL(redirectUrlPath, req.url);
-
- return c.redirect(redirectUrl);
+ // Todo: Currently not working.
+ if (!csrfToken) {
+ await setCsrfCookie(c);
}
- // Redirect `/t` to `/settings/teams`.
- if (path === '/t' || path === '/t/') {
- logger.log('Redirecting to /settings/teams');
+ // // Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
+ // const resetPreferredTeamUrl =
+ // referrerPathname &&
+ // referrerPathname.startsWith('/t/') &&
+ // (!path.startsWith('/t/') || path === '/');
- const redirectUrl = new URL('/settings/teams', req.url);
- return c.redirect(redirectUrl);
- }
+ // // Redirect root page to `/documents` or `/t/{preferredTeamUrl}/documents`.
+ // if (path === '/') {
+ // logger.log('Redirecting from root to documents');
- // Redirect `/t/` to `/t//documents`.
- if (TEAM_URL_ROOT_REGEX.test(path)) {
- logger.log('Redirecting team documents');
+ // const redirectUrlPath = formatDocumentsPath(
+ // resetPreferredTeamUrl ? undefined : preferredTeamUrl,
+ // );
- const redirectUrl = new URL(`${path}/documents`, req.url);
- setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
+ // const redirectUrl = new URL(redirectUrlPath, req.url);
- return c.redirect(redirectUrl);
- }
+ // return c.redirect(redirectUrl);
+ // }
- // Set the preferred team url cookie if user accesses a team page.
- if (path.startsWith('/t/')) {
- setCookie(c, 'preferred-team-url', path.split('/')[2]);
- return next();
- }
+ // // Redirect `/t` to `/settings/teams`.
+ // if (path === '/t' || path === '/t/') {
+ // logger.log('Redirecting to /settings/teams');
- // Clear preferred team url cookie if user accesses a non team page from a team page.
- if (resetPreferredTeamUrl || path === '/documents') {
- logger.log('Resetting preferred team url');
+ // const redirectUrl = new URL('/settings/teams', req.url);
+ // return c.redirect(redirectUrl);
+ // }
- deleteCookie(c, 'preferred-team-url');
- return next();
- }
+ // // Redirect `/t/` to `/t//documents`.
+ // if (TEAM_URL_ROOT_REGEX.test(path)) {
+ // logger.log('Redirecting team documents');
- // Todo: Test
- if (path.startsWith('/embed')) {
- const origin = req.header('Origin') ?? '*';
+ // const redirectUrl = new URL(`${path}/documents`, req.url);
+ // setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
- // Allow third parties to iframe the document.
- c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- c.header('Access-Control-Allow-Origin', origin);
- c.header('Content-Security-Policy', `frame-ancestors ${origin}`);
- c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
- c.header('X-Content-Type-Options', 'nosniff');
+ // return c.redirect(redirectUrl);
+ // }
- return next();
- }
+ // // Set the preferred team url cookie if user accesses a team page.
+ // if (path.startsWith('/t/')) {
+ // setCookie(c, 'preferred-team-url', path.split('/')[2]);
+ // return next();
+ // }
+
+ // // Clear preferred team url cookie if user accesses a non team page from a team page.
+ // if (resetPreferredTeamUrl || path === '/documents') {
+ // logger.log('Resetting preferred team url');
+
+ // deleteCookie(c, 'preferred-team-url');
+ // return next();
+ // }
return next();
};
diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts
index 83d4b8610..c904f6d6b 100644
--- a/apps/remix/server/router.ts
+++ b/apps/remix/server/router.ts
@@ -1,18 +1,15 @@
import { Hono } from 'hono';
import { contextStorage } from 'hono/context-storage';
-import { PDFDocument } from 'pdf-lib';
import { tsRestHonoApp } from '@documenso/api/hono';
import { auth } from '@documenso/auth/server';
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
-import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
-import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
-import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { openApiDocument } from '@documenso/trpc/server/open-api';
+import { filesRoute } from './api/files';
import { type AppContext, appContext } from './context';
+import { appMiddleware } from './middleware';
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
@@ -30,12 +27,17 @@ const app = new Hono();
app.use(contextStorage());
app.use(appContext);
-// App middleware.
-// app.use('*', appMiddleware);
+/**
+ * Middleware for initial page loads.
+ */
+app.use('*', appMiddleware);
// Auth server.
app.route('/api/auth', auth);
+// Files route.
+app.route('/api/files', filesRoute);
+
// API servers. Todo: Configure max durations, etc?
app.route('/api/v1', tsRestHonoApp);
app.use('/api/jobs/*', jobsClient.getApiHandler());
@@ -45,73 +47,4 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c)); // Todo: Add next()?
-// Temp uploader.
-app
- .post('/api/file', async (c) => {
- try {
- const formData = await c.req.formData();
- const file = formData.get('file') as File;
-
- if (!file) {
- return c.json({ error: 'No file provided' }, 400);
- }
-
- // Add file size validation
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
- if (file.size > MAX_FILE_SIZE) {
- return c.json({ error: 'File too large' }, 400);
- }
-
- const arrayBuffer = await file.arrayBuffer();
-
- const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
- console.error(`PDF upload parse error: ${e.message}`);
-
- throw new AppError('INVALID_DOCUMENT_FILE');
- });
-
- if (pdf.isEncrypted) {
- throw new AppError('INVALID_DOCUMENT_FILE');
- }
-
- // Todo: Test this.
- if (!file.name.endsWith('.pdf')) {
- Object.defineProperty(file, 'name', {
- writable: true,
- value: `${file.name}.pdf`,
- });
- }
-
- const { type, data } = await putFile(file);
-
- const result = await createDocumentData({ type, data });
-
- return c.json(result);
- } catch (error) {
- console.error('Upload failed:', error);
- return c.json({ error: 'Upload failed' }, 500);
- }
- })
- .get('/api/file', async (c) => {
- const key = c.req.query('key');
-
- const { url } = await getPresignGetUrl(key || '');
-
- const response = await fetch(url, {
- method: 'GET',
- });
-
- if (!response.ok) {
- throw new Error(`Failed to get file "${key}", failed with status code ${response.status}`);
- }
-
- const buffer = await response.arrayBuffer();
-
- const binaryData = new Uint8Array(buffer);
-
- return c.json({
- binaryData,
- });
- });
-
export default app;
diff --git a/apps/remix/tsconfig.json b/apps/remix/tsconfig.json
index 7587713dd..e5bb783ef 100644
--- a/apps/remix/tsconfig.json
+++ b/apps/remix/tsconfig.json
@@ -27,6 +27,7 @@
"moduleDetection": "force",
"resolveJsonModule": true,
"skipLibCheck": true,
- "strict": true
+ "strict": true,
+ "useUnknownInCatchVariables": false
}
}
diff --git a/package-lock.json b/package-lock.json
index b03e1ba93..813afeafe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -108,7 +108,6 @@
"@epic-web/remember": "^1.1.0",
"@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4",
- "@hono/zod-validator": "^0.4.2",
"@hookform/resolvers": "^3.1.0",
"@lingui/core": "^5.2.0",
"@lingui/detect-locale": "^5.2.0",
@@ -123,7 +122,7 @@
"autoprefixer": "^10.4.13",
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
- "hono": "4.6.15",
+ "hono": "4.7.0",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@@ -180,6 +179,7 @@
"typescript": "5.6.2",
"vite": "^6.1.0",
"vite-plugin-babel-macros": "^1.0.6",
+ "vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.1.4"
}
},
@@ -849,6 +849,16 @@
"win32"
]
},
+ "apps/remix/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"apps/remix/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@@ -890,6 +900,35 @@
"@esbuild/win32-x64": "0.24.2"
}
},
+ "apps/remix/node_modules/meow": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
+ "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"apps/remix/node_modules/rollup": {
"version": "4.34.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.5.tgz",
@@ -929,6 +968,88 @@
"fsevents": "~2.3.2"
}
},
+ "apps/remix/node_modules/type-fest": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
+ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "apps/remix/node_modules/vite-plugin-checker": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz",
+ "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "ansi-escapes": "^4.3.0",
+ "chalk": "^4.1.1",
+ "chokidar": "^3.5.1",
+ "commander": "^8.0.0",
+ "fast-glob": "^3.2.7",
+ "fs-extra": "^11.1.0",
+ "npm-run-path": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "tiny-invariant": "^1.1.0",
+ "vscode-languageclient": "^7.0.0",
+ "vscode-languageserver": "^7.0.0",
+ "vscode-languageserver-textdocument": "^1.0.1",
+ "vscode-uri": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "peerDependencies": {
+ "@biomejs/biome": ">=1.7",
+ "eslint": ">=7",
+ "meow": "^9.0.0",
+ "optionator": "^0.9.1",
+ "stylelint": ">=13",
+ "typescript": "*",
+ "vite": ">=2.0.0",
+ "vls": "*",
+ "vti": "*",
+ "vue-tsc": "~2.1.6"
+ },
+ "peerDependenciesMeta": {
+ "@biomejs/biome": {
+ "optional": true
+ },
+ "eslint": {
+ "optional": true
+ },
+ "meow": {
+ "optional": true
+ },
+ "optionator": {
+ "optional": true
+ },
+ "stylelint": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "vls": {
+ "optional": true
+ },
+ "vti": {
+ "optional": true
+ },
+ "vue-tsc": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1024,7 +1145,7 @@
"@auth/core": "0.28.2"
},
"peerDependencies": {
- "kysely": "^0.26.1"
+ "kysely": "^0.26.3"
}
},
"node_modules/@aws-crypto/crc32": {
@@ -3801,6 +3922,16 @@
"hono": "^4"
}
},
+ "node_modules/@hono/standard-validator": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@hono/standard-validator/-/standard-validator-0.1.2.tgz",
+ "integrity": "sha512-mVyv2fpx/o0MNAEhjXhvuVbW3BWTGnf8F4w8ZifztE+TWXjUAKr7KAOZfcDhVrurgVhKw7RbTnEog2beZM6QtQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@standard-schema/spec": "1.0.0",
+ "hono": ">=3.9.0"
+ }
+ },
"node_modules/@hono/trpc-server": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@hono/trpc-server/-/trpc-server-0.3.4.tgz",
@@ -3867,16 +3998,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@hono/zod-validator": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.4.2.tgz",
- "integrity": "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g==",
- "license": "MIT",
- "peerDependencies": {
- "hono": ">=3.9.0",
- "zod": "^3.19.1"
- }
- },
"node_modules/@hookform/resolvers": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
@@ -10688,6 +10809,13 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@swagger-api/apidom-ast": {
"version": "1.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.11.tgz",
@@ -22435,9 +22563,9 @@
"license": "MIT"
},
"node_modules/hono": {
- "version": "4.6.15",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.15.tgz",
- "integrity": "sha512-OiQwvAOAaI2JrABBH69z5rsctHDzFzIKJge0nYXgtzGJ0KftwLWcBXm1upJC23/omNRtnqM0gjRMbtXshPdqhQ==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.0.tgz",
+ "integrity": "sha512-hV97aIR4WYbG30k234sD9B3VNr1ZWdQRmrVF76LKFlmI7O9Yo70mG9+mFwyQ6Sjrz4wH71GfnBxv6CPjcx3QNw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -39965,6 +40093,69 @@
"@esbuild/win32-x64": "0.24.2"
}
},
+ "node_modules/vscode-jsonrpc": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz",
+ "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0 || >=10.0.0"
+ }
+ },
+ "node_modules/vscode-languageclient": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz",
+ "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^3.0.4",
+ "semver": "^7.3.4",
+ "vscode-languageserver-protocol": "3.16.0"
+ },
+ "engines": {
+ "vscode": "^1.52.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz",
+ "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.16.0"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz",
+ "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "6.0.0",
+ "vscode-languageserver-types": "3.16.0"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
+ "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vscode-oniguruma": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
@@ -39977,6 +40168,13 @@
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
"license": "MIT"
},
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/wait-on": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz",
@@ -40754,11 +40952,11 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
- "@hono/zod-validator": "^0.4.2",
+ "@hono/standard-validator": "^0.1.2",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"arctic": "^3.1.0",
- "hono": "4.6.15",
+ "hono": "4.7.0",
"luxon": "^3.5.0",
"nanoid": "^4.0.2",
"ts-pattern": "^5.0.5",
@@ -41706,7 +41904,7 @@
"@upstash/redis": "^1.20.6",
"@vvo/tzdb": "^6.117.0",
"inngest": "^3.19.13",
- "kysely": "^0.26.3",
+ "kysely": "0.26.3",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
@@ -41744,7 +41942,7 @@
"license": "MIT",
"dependencies": {
"@prisma/client": "^5.4.2",
- "kysely": "^0.27.3",
+ "kysely": "0.26.3",
"prisma": "^5.4.2",
"prisma-extension-kysely": "^2.1.0",
"ts-pattern": "^5.0.6"
@@ -41759,15 +41957,6 @@
"zod-prisma-types": "3.1.9"
}
},
- "packages/prisma/node_modules/kysely": {
- "version": "0.27.5",
- "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.5.tgz",
- "integrity": "sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
"packages/signing": {
"name": "@documenso/signing",
"version": "0.0.0",
@@ -42253,24 +42442,6 @@
"zod": "3.24.1"
}
},
- "packages/trpc/node_modules/@ts-rest/core": {
- "version": "3.51.0",
- "resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.51.0.tgz",
- "integrity": "sha512-v6lnWEcpZj1UgN9wb84XQ+EORP1QEtncFumoXMJjno5ZUV6vdjKze3MYcQN0C6vjBpIJPQEaI/gab2jr4/0KzQ==",
- "license": "MIT",
- "peerDependencies": {
- "@types/node": "^18.18.7 || >=20.8.4",
- "zod": "^3.22.3"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
- }
- },
"packages/trpc/node_modules/@ts-rest/next": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/@ts-rest/next/-/next-3.51.0.tgz",
@@ -42287,25 +42458,6 @@
}
}
},
- "packages/trpc/node_modules/@types/node": {
- "version": "22.13.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
- "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": "~6.20.0"
- }
- },
- "packages/trpc/node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"packages/tsconfig": {
"name": "@documenso/tsconfig",
"version": "0.0.0",
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index ac2851946..f96ce3b09 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -42,8 +42,8 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
-import { getFile } from '@documenso/lib/universal/upload/get-file';
-import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
+import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
+import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import {
getPresignGetUrl,
getPresignPostUrl,
@@ -490,14 +490,14 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
let documentDataId = document.documentDataId;
if (body.formValues) {
- const pdf = await getFile(document.documentData);
+ const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
- const newDocumentData = await putPdfFile({
+ const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
@@ -598,14 +598,14 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
- const pdf = await getFile(document.documentData);
+ const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
- const newDocumentData = await putPdfFile({
+ const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 5e93e7e46..5f8b7f957 100644
--- a/packages/auth/package.json
+++ b/packages/auth/package.json
@@ -12,11 +12,11 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
- "@hono/zod-validator": "^0.4.2",
+ "@hono/standard-validator": "^0.1.2",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"arctic": "^3.1.0",
- "hono": "4.6.15",
+ "hono": "4.7.0",
"luxon": "^3.5.0",
"nanoid": "^4.0.2",
"ts-pattern": "^5.0.5",
diff --git a/packages/auth/server/lib/session/session.ts b/packages/auth/server/lib/session/session.ts
index 85c258cb4..ef357ce0a 100644
--- a/packages/auth/server/lib/session/session.ts
+++ b/packages/auth/server/lib/session/session.ts
@@ -5,14 +5,28 @@ import { type Session, type User, UserSecurityAuditLogType } from '@prisma/clien
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
+/**
+ * The user object to pass around the app.
+ *
+ * Do not put anything sensitive in here since it will be public.
+ */
+export type SessionUser = Pick<
+ User,
+ | 'id'
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'avatarImageId'
+ | 'twoFactorEnabled'
+ | 'roles'
+ | 'signature'
+ | 'url'
+>;
+
export type SessionValidationResult =
| {
session: Session;
- user: User;
- // user: Pick<
- // User,
- // 'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' // Todo
- // >;
+ user: SessionUser;
isAuthenticated: true;
}
| { session: null; user: null; isAuthenticated: false };
@@ -36,7 +50,7 @@ export const createSession = async (
const session: Session = {
id: hashedSessionId,
- sessionToken: hashedSessionId, // todo
+ sessionToken: hashedSessionId,
userId,
updatedAt: new Date(),
createdAt: new Date(),
@@ -69,23 +83,26 @@ export const validateSessionToken = async (token: string): Promise()
/**
* Authorize endpoint.
*/
- .post('/authorize', zValidator('json', ZSignInSchema), async (c) => {
+ .post('/authorize', sValidator('json', ZSignInSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
const { email, password, totpCode, backupCode } = c.req.valid('json');
@@ -131,7 +131,7 @@ export const emailPasswordRoute = new Hono()
/**
* Signup endpoint.
*/
- .post('/signup', zValidator('json', ZSignUpSchema), async (c) => {
+ .post('/signup', sValidator('json', ZSignUpSchema), async (c) => {
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
throw new AppError('SIGNUP_DISABLED', {
message: 'Signups are disabled.',
@@ -160,7 +160,7 @@ export const emailPasswordRoute = new Hono()
/**
* Update password endpoint.
*/
- .post('/update-password', zValidator('json', ZUpdatePasswordSchema), async (c) => {
+ .post('/update-password', sValidator('json', ZUpdatePasswordSchema), async (c) => {
const { password, currentPassword } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
@@ -182,7 +182,7 @@ export const emailPasswordRoute = new Hono()
/**
* Verify email endpoint.
*/
- .post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
+ .post('/verify-email', sValidator('json', ZVerifyEmailSchema), async (c) => {
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
// If email is verified, automatically authenticate user.
@@ -197,7 +197,7 @@ export const emailPasswordRoute = new Hono()
/**
* Resend verification email endpoint.
*/
- .post('/resend-verify-email', zValidator('json', ZResendVerifyEmailSchema), async (c) => {
+ .post('/resend-verify-email', sValidator('json', ZResendVerifyEmailSchema), async (c) => {
const { email } = c.req.valid('json');
await jobsClient.triggerJob({
@@ -212,7 +212,7 @@ export const emailPasswordRoute = new Hono()
/**
* Forgot password endpoint.
*/
- .post('/forgot-password', zValidator('json', ZForgotPasswordSchema), async (c) => {
+ .post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
const { email } = c.req.valid('json');
await forgotPassword({
@@ -224,7 +224,7 @@ export const emailPasswordRoute = new Hono()
/**
* Reset password endpoint.
*/
- .post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
+ .post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
@@ -258,7 +258,7 @@ export const emailPasswordRoute = new Hono()
*/
.post(
'/2fa/enable',
- zValidator(
+ sValidator(
'json',
z.object({
code: z.string(),
@@ -304,7 +304,7 @@ export const emailPasswordRoute = new Hono()
*/
.post(
'/2fa/disable',
- zValidator(
+ sValidator(
'json',
z.object({
totpCode: z.string().trim().optional(),
@@ -325,6 +325,7 @@ export const emailPasswordRoute = new Hono()
email: true,
twoFactorEnabled: true,
twoFactorSecret: true,
+ twoFactorBackupCodes: true,
},
});
@@ -349,7 +350,7 @@ export const emailPasswordRoute = new Hono()
*/
.post(
'/2fa/view-recovery-codes',
- zValidator(
+ sValidator(
'json',
z.object({
token: z.string(),
diff --git a/packages/auth/server/routes/google.ts b/packages/auth/server/routes/google.ts
index 994964a4a..94d650863 100644
--- a/packages/auth/server/routes/google.ts
+++ b/packages/auth/server/routes/google.ts
@@ -1,4 +1,4 @@
-import { zValidator } from '@hono/zod-validator';
+import { sValidator } from '@hono/standard-validator';
import { Google, decodeIdToken, generateCodeVerifier, generateState } from 'arctic';
import { Hono } from 'hono';
import { deleteCookie, setCookie } from 'hono/cookie';
@@ -35,7 +35,7 @@ export const googleRoute = new Hono()
/**
* Authorize endpoint.
*/
- .post('/authorize', zValidator('json', ZGoogleAuthorizeSchema), (c) => {
+ .post('/authorize', sValidator('json', ZGoogleAuthorizeSchema), (c) => {
const scopes = options.scope;
const state = generateState();
diff --git a/packages/auth/server/routes/passkey.ts b/packages/auth/server/routes/passkey.ts
index fee7b1efc..c4d26e36d 100644
--- a/packages/auth/server/routes/passkey.ts
+++ b/packages/auth/server/routes/passkey.ts
@@ -1,4 +1,4 @@
-import { zValidator } from '@hono/zod-validator';
+import { sValidator } from '@hono/standard-validator';
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { Hono } from 'hono';
@@ -17,7 +17,7 @@ export const passkeyRoute = new Hono()
/**
* Authorize endpoint.
*/
- .post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
+ .post('/authorize', sValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
const { csrfToken, credential } = c.req.valid('json');
@@ -129,7 +129,7 @@ export const passkeyRoute = new Hono()
// .post(
// '/pre-authenticate',
-// zValidator(
+// sValidator(
// 'json',
// z.object({
// code: z.string(),
diff --git a/packages/email/ambient.d.ts b/packages/email/ambient.d.ts
deleted file mode 100644
index 54b8c1d7c..000000000
--- a/packages/email/ambient.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module '@documenso/tailwind-config';
diff --git a/packages/email/render.tsx b/packages/email/render.tsx
index 093e9f267..6607d80d4 100644
--- a/packages/email/render.tsx
+++ b/packages/email/render.tsx
@@ -9,6 +9,9 @@ export type RenderOptions = ReactEmail.Options & {
branding?: BrandingSettings;
};
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const colors = (config.theme?.extend?.colors || {}) as Record;
+
export const render = (element: React.ReactNode, options?: RenderOptions) => {
const { branding, ...otherOptions } = options ?? {};
@@ -17,7 +20,7 @@ export const render = (element: React.ReactNode, options?: RenderOptions) => {
config={{
theme: {
extend: {
- colors: config.theme.extend.colors,
+ colors,
},
},
}}
@@ -36,7 +39,7 @@ export const renderAsync = async (element: React.ReactNode, options?: RenderOpti
config={{
theme: {
extend: {
- colors: config.theme.extend.colors,
+ colors,
},
},
}}
diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx
index c45079663..c54e2eda9 100644
--- a/packages/email/templates/document-invite.tsx
+++ b/packages/email/templates/document-invite.tsx
@@ -31,7 +31,7 @@ export const DocumentInviteEmailTemplate = ({
role,
selfSigner = false,
isTeamInvite = false,
- teamName,
+ teamName = '',
includeSenderDetails,
}: DocumentInviteEmailTemplateProps) => {
const { _ } = useLingui();
diff --git a/packages/lib/client-only/providers/session.tsx b/packages/lib/client-only/providers/session.tsx
index cb65bdaaa..8a71a19b4 100644
--- a/packages/lib/client-only/providers/session.tsx
+++ b/packages/lib/client-only/providers/session.tsx
@@ -1,14 +1,15 @@
import { createContext, useContext } from 'react';
import React from 'react';
-import type { Session, User } from '@documenso/prisma/client';
+import type { SessionUser } from '@documenso/auth/server/lib/session/session';
+import type { Session } from '@documenso/prisma/client';
import type { TGetTeamByUrlResponse } from '../../server-only/team/get-team';
import type { TGetTeamsResponse } from '../../server-only/team/get-teams';
export type AppSession = {
session: Session;
- user: User; // Todo: Remove password, and redundant fields.
+ user: SessionUser;
currentTeam: TGetTeamByUrlResponse | null;
teams: TGetTeamsResponse;
};
diff --git a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
index 40420dc2e..fc8ce2148 100644
--- a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
@@ -114,9 +114,11 @@ export const run = async ({
emailMessage = customEmail?.message ?? '';
if (!emailMessage) {
+ const inviterName = user.name || '';
+
emailMessage = i18n._(
team.teamGlobalSettings?.includeSenderDetails
- ? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
+ ? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts
index edf6be442..dfcf6dacc 100644
--- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts
+++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts
@@ -19,8 +19,8 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
-import { getFile } from '../../../universal/upload/get-file';
-import { putPdfFile } from '../../../universal/upload/put-file';
+import { getFileServerSide } from '../../../universal/upload/get-file.server';
+import { putPdfFileServerSide } from '../../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import type { JobRunIO } from '../../client/_internal/job';
@@ -114,7 +114,7 @@ export const run = async ({
documentData.data = documentData.initialData;
}
- const pdfData = await getFile(documentData);
+ const pdfData = await getFileServerSide(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
@@ -160,7 +160,7 @@ export const run = async ({
const { name } = path.parse(document.title);
- const documentData = await putPdfFile({
+ const documentData = await putPdfFileServerSide({
name: `${name}_signed.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer),
diff --git a/packages/lib/package.json b/packages/lib/package.json
index 6c7f26169..c9a87722d 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -38,7 +38,7 @@
"@upstash/redis": "^1.20.6",
"@vvo/tzdb": "^6.117.0",
"inngest": "^3.19.13",
- "kysely": "^0.26.3",
+ "kysely": "0.26.3",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
diff --git a/packages/lib/server-only/2fa/disable-2fa.ts b/packages/lib/server-only/2fa/disable-2fa.ts
index c2c221661..14f055614 100644
--- a/packages/lib/server-only/2fa/disable-2fa.ts
+++ b/packages/lib/server-only/2fa/disable-2fa.ts
@@ -8,7 +8,10 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { validateTwoFactorAuthentication } from './validate-2fa';
type DisableTwoFactorAuthenticationOptions = {
- user: Pick;
+ user: Pick<
+ User,
+ 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
+ >;
totpCode?: string;
backupCode?: string;
requestMetadata?: RequestMetadata;
diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/document/create-document-v2.ts
index 0404b6570..a28270350 100644
--- a/packages/lib/server-only/document/create-document-v2.ts
+++ b/packages/lib/server-only/document/create-document-v2.ts
@@ -24,8 +24,8 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
-import { getFile } from '../../universal/upload/get-file';
-import { putPdfFile } from '../../universal/upload/put-file';
+import { getFileServerSide } from '../../universal/upload/get-file.server';
+import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -97,11 +97,11 @@ export const createDocumentV2 = async ({
});
if (documentData) {
- const buffer = await getFile(documentData);
+ const buffer = await getFileServerSide(documentData);
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
- const newDocumentData = await putPdfFile({
+ const newDocumentData = await putPdfFileServerSide({
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(normalizedPdf),
diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts
index e8c31f6ac..12253606f 100644
--- a/packages/lib/server-only/document/create-document.ts
+++ b/packages/lib/server-only/document/create-document.ts
@@ -13,8 +13,8 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
-import { getFile } from '../../universal/upload/get-file';
-import { putPdfFile } from '../../universal/upload/put-file';
+import { getFileServerSide } from '../../universal/upload/get-file.server';
+import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -96,11 +96,11 @@ export const createDocument = async ({
});
if (documentData) {
- const buffer = await getFile(documentData);
+ const buffer = await getFileServerSide(documentData);
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
- const newDocumentData = await putPdfFile({
+ const newDocumentData = await putPdfFileServerSide({
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(normalizedPdf),
diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts
index 9b0ef2845..4d4d2dea1 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -14,8 +14,8 @@ import {
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
-import { getFile } from '../../universal/upload/get-file';
-import { putPdfFile } from '../../universal/upload/put-file';
+import { getFileServerSide } from '../../universal/upload/get-file.server';
+import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
@@ -102,7 +102,7 @@ export const sealDocument = async ({
}
// !: Need to write the fields onto the document as a hard copy
- const pdfData = await getFile(documentData);
+ const pdfData = await getFileServerSide(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
@@ -142,7 +142,7 @@ export const sealDocument = async ({
const { name } = path.parse(document.title);
- const { data: newData } = await putPdfFile({
+ const { data: newData } = await putPdfFileServerSide({
name: `${name}_signed.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer),
diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts
index 054f64c3b..2ec306962 100644
--- a/packages/lib/server-only/document/send-completed-email.ts
+++ b/packages/lib/server-only/document/send-completed-email.ts
@@ -12,7 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
-import { getFile } from '../../universal/upload/get-file';
+import { getFileServerSide } from '../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
@@ -57,7 +57,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
const { user: owner } = document;
- const completedDocument = await getFile(document.documentData);
+ const completedDocument = await getFileServerSide(document.documentData);
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx
index 69126afa1..0832d5fa7 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -9,7 +9,6 @@ import {
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
-import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
@@ -19,7 +18,8 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
-import { getFile } from '../../universal/upload/get-file';
+import { getFileServerSide } from '../../universal/upload/get-file.server';
+import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -100,7 +100,7 @@ export const sendDocument = async ({
}
if (document.formValues) {
- const file = await getFile(documentData);
+ const file = await getFileServerSide(documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(file),
@@ -114,7 +114,7 @@ export const sendDocument = async ({
fileName = `${document.title}.pdf`;
}
- const newDocumentData = await putPdfFile({
+ const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
diff --git a/packages/lib/server-only/template/get-template-by-direct-link-token.ts b/packages/lib/server-only/template/get-template-by-direct-link-token.ts
index 83d1c59b6..04efba70e 100644
--- a/packages/lib/server-only/template/get-template-by-direct-link-token.ts
+++ b/packages/lib/server-only/template/get-template-by-direct-link-token.ts
@@ -1,5 +1,7 @@
import { prisma } from '@documenso/prisma';
+import { AppError, AppErrorCode } from '../../errors/app-error';
+
export interface GetTemplateByDirectLinkTokenOptions {
token: string;
}
@@ -7,7 +9,7 @@ export interface GetTemplateByDirectLinkTokenOptions {
export const getTemplateByDirectLinkToken = async ({
token,
}: GetTemplateByDirectLinkTokenOptions) => {
- const template = await prisma.template.findFirstOrThrow({
+ const template = await prisma.template.findFirst({
where: {
directLink: {
token,
@@ -26,8 +28,16 @@ export const getTemplateByDirectLinkToken = async ({
},
});
+ const directLink = template?.directLink;
+
+ // Doing this to enforce type safety for directLink.
+ if (!directLink) {
+ throw new AppError(AppErrorCode.NOT_FOUND);
+ }
+
return {
...template,
+ directLink,
fields: template.recipients.map((recipient) => recipient.fields).flat(),
};
};
diff --git a/packages/lib/universal/upload/get-file.server.ts b/packages/lib/universal/upload/get-file.server.ts
new file mode 100644
index 000000000..7ae8be3bc
--- /dev/null
+++ b/packages/lib/universal/upload/get-file.server.ts
@@ -0,0 +1,50 @@
+import { DocumentDataType } from '@prisma/client';
+import { base64 } from '@scure/base';
+import { match } from 'ts-pattern';
+
+export type GetFileOptions = {
+ type: DocumentDataType;
+ data: string;
+};
+
+export const getFileServerSide = async ({ type, data }: GetFileOptions) => {
+ return await match(type)
+ .with(DocumentDataType.BYTES, () => getFileFromBytes(data))
+ .with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data))
+ .with(DocumentDataType.S3_PATH, async () => getFileFromS3(data))
+ .exhaustive();
+};
+
+const getFileFromBytes = (data: string) => {
+ const encoder = new TextEncoder();
+
+ const binaryData = encoder.encode(data);
+
+ return binaryData;
+};
+
+const getFileFromBytes64 = (data: string) => {
+ const binaryData = base64.decode(data);
+
+ return binaryData;
+};
+
+const getFileFromS3 = async (key: string) => {
+ const { getPresignGetUrl } = await import('./server-actions');
+
+ const { url } = await getPresignGetUrl(key);
+
+ const response = await fetch(url, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to get file "${key}", failed with status code ${response.status}`);
+ }
+
+ const buffer = await response.arrayBuffer();
+
+ const binaryData = new Uint8Array(buffer);
+
+ return binaryData;
+};
diff --git a/packages/lib/universal/upload/get-file.ts b/packages/lib/universal/upload/get-file.ts
index c54353650..6ac631c13 100644
--- a/packages/lib/universal/upload/get-file.ts
+++ b/packages/lib/universal/upload/get-file.ts
@@ -30,9 +30,23 @@ const getFileFromBytes64 = (data: string) => {
};
const getFileFromS3 = async (key: string) => {
- const { getPresignGetUrl } = await import('./server-actions');
+ const getPresignedUrlResponse = await fetch(`/api/files/presigned-get-url`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ key,
+ }),
+ });
- const { url } = await getPresignGetUrl(key);
+ if (!getPresignedUrlResponse.ok) {
+ throw new Error(
+ `Failed to get presigned url with key "${key}", failed with status code ${getPresignedUrlResponse.status}`,
+ );
+ }
+
+ const { url } = await getPresignedUrlResponse.json();
const response = await fetch(url, {
method: 'GET',
diff --git a/packages/lib/universal/upload/put-file.server.ts b/packages/lib/universal/upload/put-file.server.ts
new file mode 100644
index 000000000..4173bdb81
--- /dev/null
+++ b/packages/lib/universal/upload/put-file.server.ts
@@ -0,0 +1,85 @@
+import { DocumentDataType } from '@prisma/client';
+import { base64 } from '@scure/base';
+import { PDFDocument } from 'pdf-lib';
+import { match } from 'ts-pattern';
+
+import { env } from '@documenso/lib/utils/env';
+
+import { AppError } from '../../errors/app-error';
+import { createDocumentData } from '../../server-only/document-data/create-document-data';
+import { uploadS3File } from './server-actions';
+
+type File = {
+ name: string;
+ type: string;
+ arrayBuffer: () => Promise;
+};
+
+/**
+ * Uploads a document file to the appropriate storage location and creates
+ * a document data record.
+ */
+export const putPdfFileServerSide = async (file: File) => {
+ const isEncryptedDocumentsAllowed = false; // Was feature flag.
+
+ const arrayBuffer = await file.arrayBuffer();
+
+ const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
+ console.error(`PDF upload parse error: ${e.message}`);
+
+ throw new AppError('INVALID_DOCUMENT_FILE');
+ });
+
+ if (!isEncryptedDocumentsAllowed && pdf.isEncrypted) {
+ throw new AppError('INVALID_DOCUMENT_FILE');
+ }
+
+ if (!file.name.endsWith('.pdf')) {
+ file.name = `${file.name}.pdf`;
+ }
+
+ const { type, data } = await putFileServerSide(file);
+
+ return await createDocumentData({ type, data });
+};
+
+/**
+ * Uploads a file to the appropriate storage location.
+ */
+export const putFileServerSide = async (file: File) => {
+ const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
+
+ return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
+ .with('s3', async () => putFileInS3(file))
+ .otherwise(async () => putFileInDatabase(file));
+};
+
+const putFileInDatabase = async (file: File) => {
+ const contents = await file.arrayBuffer();
+
+ const binaryData = new Uint8Array(contents);
+
+ const asciiData = base64.encode(binaryData);
+
+ return {
+ type: DocumentDataType.BYTES_64,
+ data: asciiData,
+ };
+};
+
+const putFileInS3 = async (file: File) => {
+ const buffer = await file.arrayBuffer();
+
+ const blob = new Blob([buffer], { type: file.type });
+
+ const newFile = new File([blob], file.name, {
+ type: file.type,
+ });
+
+ const { key } = await uploadS3File(newFile);
+
+ return {
+ type: DocumentDataType.S3_PATH,
+ data: key,
+ };
+};
diff --git a/packages/lib/universal/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts
index 15206a3bc..64f8d9be9 100644
--- a/packages/lib/universal/upload/put-file.ts
+++ b/packages/lib/universal/upload/put-file.ts
@@ -1,12 +1,15 @@
import { DocumentDataType } from '@prisma/client';
import { base64 } from '@scure/base';
-import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
import { env } from '@documenso/lib/utils/env';
+import type {
+ TGetPresignedPostUrlResponse,
+ TUploadPdfResponse,
+} from '@documenso/remix/server/api/files.types';
+import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError } from '../../errors/app-error';
-import { createDocumentData } from '../../server-only/document-data/create-document-data';
type File = {
name: string;
@@ -14,32 +17,29 @@ type File = {
arrayBuffer: () => Promise;
};
-/**
- * Uploads a document file to the appropriate storage location and creates
- * a document data record.
- */
export const putPdfFile = async (file: File) => {
- const isEncryptedDocumentsAllowed = false; // Was feature flag.
+ const formData = new FormData();
- const arrayBuffer = await file.arrayBuffer();
+ // Create a proper File object from the data
+ const buffer = await file.arrayBuffer();
+ const blob = new Blob([buffer], { type: file.type });
+ const properFile = new File([blob], file.name, { type: file.type });
- const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
- console.error(`PDF upload parse error: ${e.message}`);
+ formData.append('file', properFile);
- throw new AppError('INVALID_DOCUMENT_FILE');
+ const response = await fetch('/api/files/upload-pdf', {
+ method: 'POST',
+ body: formData,
});
- if (!isEncryptedDocumentsAllowed && pdf.isEncrypted) {
- throw new AppError('INVALID_DOCUMENT_FILE');
+ if (!response.ok) {
+ console.error('Upload failed:', response.statusText);
+ throw new AppError('UPLOAD_FAILED');
}
- if (!file.name.endsWith('.pdf')) {
- file.name = `${file.name}.pdf`;
- }
+ const result: TUploadPdfResponse = await response.json();
- const { type, data } = await putFile(file);
-
- return await createDocumentData({ type, data });
+ return result;
};
/**
@@ -67,9 +67,27 @@ const putFileInDatabase = async (file: File) => {
};
const putFileInS3 = async (file: File) => {
- const { getPresignPostUrl } = await import('./server-actions');
+ const getPresignedUrlResponse = await fetch(
+ `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/presigned-post-url`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ fileName: file.name,
+ contentType: file.type,
+ }),
+ },
+ );
- const { url, key } = await getPresignPostUrl(file.name, file.type);
+ if (!getPresignedUrlResponse.ok) {
+ throw new Error(
+ `Failed to get presigned post url, failed with status code ${getPresignedUrlResponse.status}`,
+ );
+ }
+
+ const { url, key }: TGetPresignedPostUrlResponse = await getPresignedUrlResponse.json();
const body = await file.arrayBuffer();
diff --git a/packages/lib/universal/upload/server-actions.ts b/packages/lib/universal/upload/server-actions.ts
index 3baee5b04..79bc90090 100644
--- a/packages/lib/universal/upload/server-actions.ts
+++ b/packages/lib/universal/upload/server-actions.ts
@@ -9,37 +9,21 @@ import path from 'node:path';
import { env } from '@documenso/lib/utils/env';
-import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { alphaid } from '../id';
-export const getPresignPostUrl = async (fileName: string, contentType: string) => {
+export const getPresignPostUrl = async (fileName: string, contentType: string, userId?: number) => {
const client = getS3Client();
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
- const token: { id: string } | null = null;
-
- try {
- const baseUrl = NEXT_PUBLIC_WEBAPP_URL();
-
- // Todo
- // token = await getToken({
- // req: new NextRequest(baseUrl, {
- // headers: headers(),
- // }),
- // });
- } catch (err) {
- // Non server-component environment
- }
-
// Get the basename and extension for the file
const { name, ext } = path.parse(fileName);
let key = `${alphaid(12)}/${slugify(name)}${ext}`;
- if (token) {
- key = `${token.id}/${key}`;
+ if (userId) {
+ key = `${userId}/${key}`;
}
const putObjectCommand = new PutObjectCommand({
@@ -104,6 +88,31 @@ export const getPresignGetUrl = async (key: string) => {
return { key, url };
};
+/**
+ * Uploads a file to S3.
+ */
+export const uploadS3File = async (file: File) => {
+ const client = getS3Client();
+
+ // Get the basename and extension for the file
+ const { name, ext } = path.parse(file.name);
+
+ const key = `${alphaid(12)}/${slugify(name)}${ext}`;
+
+ const fileBuffer = await file.arrayBuffer();
+
+ const response = await client.send(
+ new PutObjectCommand({
+ Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
+ Key: key,
+ Body: Buffer.from(fileBuffer),
+ ContentType: file.type,
+ }),
+ );
+
+ return { key, response };
+};
+
export const deleteS3File = async (key: string) => {
const client = getS3Client();
diff --git a/packages/prisma/package.json b/packages/prisma/package.json
index e03e99183..64d91b0a6 100644
--- a/packages/prisma/package.json
+++ b/packages/prisma/package.json
@@ -22,7 +22,7 @@
},
"dependencies": {
"@prisma/client": "^5.4.2",
- "kysely": "^0.27.3",
+ "kysely": "0.26.3",
"prisma": "^5.4.2",
"prisma-extension-kysely": "^2.1.0",
"ts-pattern": "^5.0.6"
diff --git a/packages/tailwind-config/index.d.ts b/packages/tailwind-config/index.d.ts
new file mode 100644
index 000000000..3255d5393
--- /dev/null
+++ b/packages/tailwind-config/index.d.ts
@@ -0,0 +1,4 @@
+import type { Config } from 'tailwindcss';
+
+declare const config: Config;
+export default config;
diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json
index 35d7816c1..08e6e60c3 100644
--- a/packages/tailwind-config/package.json
+++ b/packages/tailwind-config/package.json
@@ -2,6 +2,7 @@
"name": "@documenso/tailwind-config",
"version": "0.0.0",
"main": "index.cjs",
+ "types": "index.d.ts",
"license": "MIT",
"scripts": {
"clean": "rimraf node_modules"
@@ -18,4 +19,4 @@
"publishConfig": {
"access": "public"
}
-}
\ No newline at end of file
+}
diff --git a/packages/trpc/server/context.ts b/packages/trpc/server/context.ts
index 9a33ee48a..21ef81487 100644
--- a/packages/trpc/server/context.ts
+++ b/packages/trpc/server/context.ts
@@ -1,10 +1,11 @@
import type { Context } from 'hono';
import { z } from 'zod';
+import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
-import type { Session, User } from '@documenso/prisma/client';
+import type { Session } from '@documenso/prisma/client';
type CreateTrpcContextOptions = {
c: Context;
@@ -59,7 +60,7 @@ export type TrpcContext = (
}
| {
session: Session;
- user: User;
+ user: SessionUser;
}
) & {
teamId: number | undefined;
diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx
index 5d606d3da..49653c516 100644
--- a/packages/ui/primitives/document-flow/add-fields.tsx
+++ b/packages/ui/primitives/document-flow/add-fields.tsx
@@ -67,13 +67,6 @@ import { FieldAdvancedSettings } from './field-item-advanced-settings';
import { MissingSignatureFieldDialog } from './missing-signature-field-dialog';
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
-// const fontCaveat = Caveat({
-// weight: ['500'],
-// subsets: ['latin'],
-// display: 'swap',
-// variable: '--font-caveat',
-// });
-
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@@ -541,12 +534,6 @@ export const AddFieldsFormPartial = ({
);
}, [recipientsByRole]);
- const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
-
- const handleTypedSignatureChange = (value: boolean) => {
- form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
- };
-
const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev);
};
@@ -831,8 +818,7 @@ export const AddFieldsFormPartial = ({
Signature
diff --git a/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx b/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx
index 5fb8c1058..d0ff41e5f 100644
--- a/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx
+++ b/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx
@@ -20,15 +20,13 @@ export const CheckboxField = ({ field }: CheckboxFieldProps) => {
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
- return (
-
- );
+ return ;
}
return (
{!parsedFieldMeta?.values ? (
-
+
) : (
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
diff --git a/packages/ui/primitives/document-flow/advanced-fields/radio.tsx b/packages/ui/primitives/document-flow/advanced-fields/radio.tsx
index 356aebfd8..28af5a10f 100644
--- a/packages/ui/primitives/document-flow/advanced-fields/radio.tsx
+++ b/packages/ui/primitives/document-flow/advanced-fields/radio.tsx
@@ -20,15 +20,13 @@ export const RadioField = ({ field }: RadioFieldProps) => {
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
- return (
-
- );
+ return
;
}
return (
{!parsedFieldMeta?.values ? (
-
+
) : (
{parsedFieldMeta.values?.map((item, index) => (
diff --git a/packages/ui/primitives/password-input.tsx b/packages/ui/primitives/password-input.tsx
index 502344a02..5925ceee5 100644
--- a/packages/ui/primitives/password-input.tsx
+++ b/packages/ui/primitives/password-input.tsx
@@ -4,7 +4,8 @@ import { Eye, EyeOff } from 'lucide-react';
import { cn } from '../lib/utils';
import { Button } from './button';
-import { Input, InputProps } from './input';
+import type { InputProps } from './input';
+import { Input } from './input';
const PasswordInput = React.forwardRef>(
({ className, ...props }, ref) => {
diff --git a/packages/ui/primitives/signature-pad/point.ts b/packages/ui/primitives/signature-pad/point.ts
index 54322f2fb..0baf47972 100644
--- a/packages/ui/primitives/signature-pad/point.ts
+++ b/packages/ui/primitives/signature-pad/point.ts
@@ -1,4 +1,4 @@
-import {
+import type {
MouseEvent as ReactMouseEvent,
PointerEvent as ReactPointerEvent,
TouchEvent as ReactTouchEvent,
diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx
index 868912efc..54865a411 100644
--- a/packages/ui/primitives/signature-pad/signature-pad.tsx
+++ b/packages/ui/primitives/signature-pad/signature-pad.tsx
@@ -20,14 +20,6 @@ import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
-// Todo
-// const fontCaveat = Caveat({
-// weight: ['500'],
-// subsets: ['latin'],
-// display: 'swap',
-// variable: '--font-caveat',
-// });
-
const DPI = 2;
const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,');
@@ -309,8 +301,7 @@ export const SignaturePad = ({
if (ctx) {
const canvasWidth = $el.current.width;
const canvasHeight = $el.current.height;
- // const fontFamily = String(fontCaveat.style.fontFamily);
- const fontFamily = 'sans-serif';
+ const fontFamily = 'Caveat';
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.textAlign = 'center';
diff --git a/packages/ui/primitives/template-flow/add-template-fields.tsx b/packages/ui/primitives/template-flow/add-template-fields.tsx
index 885f28649..5d1190e3a 100644
--- a/packages/ui/primitives/template-flow/add-template-fields.tsx
+++ b/packages/ui/primitives/template-flow/add-template-fields.tsx
@@ -61,13 +61,6 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form'
import { useStep } from '../stepper';
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
-// const fontCaveat = Caveat({
-// weight: ['500'],
-// subsets: ['latin'],
-// display: 'swap',
-// variable: '--font-caveat',
-// });
-
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@@ -696,8 +689,7 @@ export const AddTemplateFieldsFormPartial = ({
Signature