mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
fix: wip
This commit is contained in:
110
apps/remix/server/api/files.ts
Normal file
110
apps/remix/server/api/files.ts
Normal file
@ -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<HonoEnv>()
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
38
apps/remix/server/api/files.types.ts
Normal file
38
apps/remix/server/api/files.types.ts
Normal file
@ -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<typeof ZUploadPdfRequestSchema>;
|
||||
export type TUploadPdfResponse = z.infer<typeof ZUploadPdfResponseSchema>;
|
||||
|
||||
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<typeof ZGetPresignedPostUrlRequestSchema>;
|
||||
export type TGetPresignedPostUrlResponse = z.infer<typeof ZGetPresignedPostUrlResponseSchema>;
|
||||
export type TGetPresignedGetUrlRequest = z.infer<typeof ZGetPresignedGetUrlRequestSchema>;
|
||||
export type TGetPresignedGetUrlResponse = z.infer<typeof ZGetPresignedGetUrlResponseSchema>;
|
||||
@ -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/<team_url>` to `/t/<team_url>/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/<team_url>` to `/t/<team_url>/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();
|
||||
};
|
||||
|
||||
@ -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<HonoEnv>();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user