mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 19:21:39 +10:00
fix: wip
This commit is contained in:
@ -3,17 +3,17 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/public/fonts/inter-regular.ttf') format('ttf');
|
||||
font-weight: 400;
|
||||
/* font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-display: swap; */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Caveat';
|
||||
src: url('/public/fonts/caveat.ttf') format('ttf');
|
||||
font-weight: 400;
|
||||
/* font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-display: swap; */
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@ -162,6 +162,8 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
console.error('[RootErrorBoundary]', error);
|
||||
|
||||
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
||||
|
||||
return <GenericErrorLayout errorCode={errorCode} />;
|
||||
|
||||
@ -18,13 +18,15 @@ export const loader = async ({ request, context }: Route.LoaderArgs) => {
|
||||
throw redirect('/signin');
|
||||
}
|
||||
|
||||
const banner = await getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
);
|
||||
|
||||
const requestHeaders = Object.fromEntries(request.headers.entries());
|
||||
|
||||
const limits = await getLimits({ headers: requestHeaders, teamId: session.currentTeam?.id });
|
||||
// Todo: Should only load this on first render.
|
||||
const [limits, banner] = await Promise.all([
|
||||
getLimits({ headers: requestHeaders, teamId: session.currentTeam?.id }),
|
||||
getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
|
||||
@ -11,11 +11,15 @@ 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 { appMiddleware } from './middleware';
|
||||
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
||||
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// App middleware.
|
||||
app.use('*', appMiddleware);
|
||||
|
||||
// Auth server.
|
||||
app.route('/api/auth', auth);
|
||||
|
||||
@ -26,7 +30,7 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
|
||||
|
||||
// Unstable API server routes. Order matters for these two.
|
||||
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
||||
app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c));
|
||||
app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c)); // Todo: Add next()?
|
||||
|
||||
// Temp uploader.
|
||||
app
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { extractSessionCookieFromHeaders } from '@documenso/auth/server/lib/session/session-cookies';
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { type TGetTeamsResponse, getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { AppLogger } from '@documenso/lib/utils/debugger';
|
||||
|
||||
type GetLoadContextArgs = {
|
||||
request: Request;
|
||||
@ -11,16 +13,19 @@ declare module 'react-router' {
|
||||
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
|
||||
}
|
||||
|
||||
const logger = new AppLogger('[Context]');
|
||||
|
||||
export async function getLoadContext(args: GetLoadContextArgs) {
|
||||
const initTime = Date.now();
|
||||
|
||||
const request = args.request;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Todo only make available for get requests (loaders) and non api routes
|
||||
// use config
|
||||
if (request.method !== 'GET' || !config.matcher.test(url.pathname)) {
|
||||
console.log('[Session]: Pathname ignored', url.pathname);
|
||||
const noSessionCookie = extractSessionCookieFromHeaders(request.headers) === null;
|
||||
|
||||
if (!isPageRequest(request) || noSessionCookie) {
|
||||
logger.log('Pathname ignored', url.pathname);
|
||||
|
||||
return {
|
||||
requestMetadata: extractRequestMetadata(request),
|
||||
session: null,
|
||||
@ -30,27 +35,28 @@ export async function getLoadContext(args: GetLoadContextArgs) {
|
||||
const splitUrl = url.pathname.split('/');
|
||||
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
let teams: TGetTeamsResponse = [];
|
||||
|
||||
const session = await getSession(args.request);
|
||||
|
||||
if (session.isAuthenticated && splitUrl[1] === 't' && splitUrl[2]) {
|
||||
const teamUrl = splitUrl[2];
|
||||
|
||||
team = await getTeamByUrl({ userId: session.user.id, teamUrl });
|
||||
}
|
||||
|
||||
let teams: TGetTeamsResponse = [];
|
||||
|
||||
if (session.isAuthenticated) {
|
||||
// This is always loaded for the header.
|
||||
teams = await getTeams({ userId: session.user.id });
|
||||
let teamUrl = null;
|
||||
|
||||
if (splitUrl[1] === 't' && splitUrl[2]) {
|
||||
teamUrl = splitUrl[2];
|
||||
}
|
||||
|
||||
const result = await Promise.all([
|
||||
getTeams({ userId: session.user.id }),
|
||||
teamUrl ? getTeamByUrl({ userId: session.user.id, teamUrl }) : null,
|
||||
]);
|
||||
|
||||
teams = result[0];
|
||||
team = result[1];
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(`[Session]: Pathname accepted in ${endTime - initTime}ms`, url.pathname);
|
||||
|
||||
// Todo: Optimise and chain promises.
|
||||
// Todo: This is server only right?? Results not exposed?
|
||||
logger.log(`Pathname accepted in ${endTime - initTime}ms`, url.pathname);
|
||||
|
||||
return {
|
||||
requestMetadata: extractRequestMetadata(request),
|
||||
@ -65,17 +71,20 @@ export async function getLoadContext(args: GetLoadContextArgs) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route matcher configuration that excludes common non-route paths:
|
||||
* - /api/* (API routes)
|
||||
* - /assets/* (Static assets)
|
||||
* - /build/* (Build output)
|
||||
* - /favicon.* (Favicon files)
|
||||
* - *.webmanifest (Web manifest files)
|
||||
* - Paths starting with . (e.g. .well-known)
|
||||
*/
|
||||
const config = {
|
||||
matcher: new RegExp(
|
||||
'/((?!api|assets|static|build|favicon|__manifest|site.webmanifest|manifest.webmanifest|\\..*).*)',
|
||||
),
|
||||
const isPageRequest = (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.pathname.endsWith('.data')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.headers.get('Accept')?.includes('text/html')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
93
apps/remix/server/middleware.ts
Normal file
93
apps/remix/server/middleware.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { Context, Next } from 'hono';
|
||||
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
|
||||
|
||||
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
||||
import { AppLogger } from '@documenso/lib/utils/debugger';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
const logger = new AppLogger('Middleware');
|
||||
|
||||
export const appMiddleware = async (c: Context, next: Next) => {
|
||||
const { req } = c;
|
||||
const { path } = req;
|
||||
|
||||
// Basic paths to ignore.
|
||||
if (path.startsWith('/api') || path.endsWith('.data') || path.startsWith('/__manifest')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
logger.log('Path', path);
|
||||
|
||||
const preferredTeamUrl = getCookie(c, 'preferred-team-url');
|
||||
|
||||
const referrer = c.req.header('referer');
|
||||
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 === '/');
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Redirect `/t` to `/settings/teams`.
|
||||
if (path === '/t' || path === '/t/') {
|
||||
logger.log('Redirecting to /settings/teams');
|
||||
|
||||
const redirectUrl = new URL('/settings/teams', req.url);
|
||||
return c.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
// Redirect `/t/<team_url>` to `/t/<team_url>/documents`.
|
||||
if (TEAM_URL_ROOT_REGEX.test(path)) {
|
||||
logger.log('Redirecting team documents');
|
||||
|
||||
const redirectUrl = new URL(`${path}/documents`, req.url);
|
||||
setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Todo: Test
|
||||
if (path.startsWith('/embed')) {
|
||||
const origin = req.header('Origin') ?? '*';
|
||||
|
||||
// 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 next();
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@ -3,17 +3,16 @@ import { createOpenApiFetchHandler } from 'trpc-to-openapi';
|
||||
|
||||
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
||||
import { createTrpcContext } from '@documenso/trpc/server/context';
|
||||
import { appRouter } from '@documenso/trpc/server/router';
|
||||
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
||||
|
||||
import { createHonoTrpcContext } from './trpc-context';
|
||||
|
||||
export const openApiTrpcServerHandler = async (c: Context) => {
|
||||
return createOpenApiFetchHandler<typeof appRouter>({
|
||||
endpoint: API_V2_BETA_URL,
|
||||
router: appRouter,
|
||||
// Todo: Test this, since it's not using the createContext params.
|
||||
createContext: async () => createHonoTrpcContext({ c, requestSource: 'apiV2' }),
|
||||
createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }),
|
||||
req: c.req.raw,
|
||||
onError: (opts) => handleTrpcRouterError(opts, 'apiV2'),
|
||||
// Not sure why we need to do this since we handle it in errorFormatter which runs after this.
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { trpcServer } from '@hono/trpc-server';
|
||||
|
||||
import { createTrpcContext } from '@documenso/trpc/server/context';
|
||||
import { appRouter } from '@documenso/trpc/server/router';
|
||||
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
||||
|
||||
import { createHonoTrpcContext } from './trpc-context';
|
||||
|
||||
// Todo
|
||||
// export const config = {
|
||||
// maxDuration: 120,
|
||||
@ -21,6 +20,6 @@ import { createHonoTrpcContext } from './trpc-context';
|
||||
export const reactRouterTrpcServer = trpcServer({
|
||||
router: appRouter,
|
||||
endpoint: '/api/trpc',
|
||||
createContext: async (_, c) => createHonoTrpcContext({ c, requestSource: 'app' }),
|
||||
createContext: async (_, c) => createTrpcContext({ c, requestSource: 'app' }),
|
||||
onError: (opts) => handleTrpcRouterError(opts, 'trpc'),
|
||||
});
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
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 { TrpcContext } from '@documenso/trpc/server/context';
|
||||
|
||||
type CreateTrpcContextOptions = {
|
||||
c: Context;
|
||||
requestSource: 'app' | 'apiV1' | 'apiV2';
|
||||
};
|
||||
|
||||
/**
|
||||
* For trpc that uses @documenso/auth and Hono.
|
||||
*/
|
||||
export const createHonoTrpcContext = async ({
|
||||
c,
|
||||
requestSource,
|
||||
}: CreateTrpcContextOptions): Promise<TrpcContext> => {
|
||||
const { session, user } = await getSession(c);
|
||||
|
||||
const req = c.req.raw;
|
||||
|
||||
const metadata: ApiRequestMetadata = {
|
||||
requestMetadata: extractRequestMetadata(req),
|
||||
source: requestSource,
|
||||
auth: null,
|
||||
};
|
||||
|
||||
const rawTeamId = req.headers.get('x-team-id') || undefined;
|
||||
|
||||
const teamId = z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
.catch(() => undefined)
|
||||
.parse(rawTeamId);
|
||||
|
||||
if (!session || !user) {
|
||||
return {
|
||||
session: null,
|
||||
user: null,
|
||||
teamId,
|
||||
req,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
teamId,
|
||||
req,
|
||||
metadata,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user