mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
fix: wip
This commit is contained in:
@ -3,17 +3,17 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
src: url('/public/fonts/inter-regular.ttf') format('ttf');
|
src: url('/public/fonts/inter-regular.ttf') format('ttf');
|
||||||
font-weight: 400;
|
/* font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap; */
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Caveat';
|
font-family: 'Caveat';
|
||||||
src: url('/public/fonts/caveat.ttf') format('ttf');
|
src: url('/public/fonts/caveat.ttf') format('ttf');
|
||||||
font-weight: 400;
|
/* font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap; */
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@ -162,6 +162,8 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
console.error('[RootErrorBoundary]', error);
|
||||||
|
|
||||||
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
||||||
|
|
||||||
return <GenericErrorLayout errorCode={errorCode} />;
|
return <GenericErrorLayout errorCode={errorCode} />;
|
||||||
|
|||||||
@ -18,13 +18,15 @@ export const loader = async ({ request, context }: Route.LoaderArgs) => {
|
|||||||
throw redirect('/signin');
|
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 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 {
|
return {
|
||||||
user: session.user,
|
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 { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
||||||
|
|
||||||
|
import { appMiddleware } from './middleware';
|
||||||
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
||||||
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
|
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
// App middleware.
|
||||||
|
app.use('*', appMiddleware);
|
||||||
|
|
||||||
// Auth server.
|
// Auth server.
|
||||||
app.route('/api/auth', auth);
|
app.route('/api/auth', auth);
|
||||||
|
|
||||||
@ -26,7 +30,7 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
|
|||||||
|
|
||||||
// Unstable API server routes. Order matters for these two.
|
// Unstable API server routes. Order matters for these two.
|
||||||
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
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.
|
// Temp uploader.
|
||||||
app
|
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 { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
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 { type TGetTeamsResponse, getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import { AppLogger } from '@documenso/lib/utils/debugger';
|
||||||
|
|
||||||
type GetLoadContextArgs = {
|
type GetLoadContextArgs = {
|
||||||
request: Request;
|
request: Request;
|
||||||
@ -11,16 +13,19 @@ declare module 'react-router' {
|
|||||||
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
|
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = new AppLogger('[Context]');
|
||||||
|
|
||||||
export async function getLoadContext(args: GetLoadContextArgs) {
|
export async function getLoadContext(args: GetLoadContextArgs) {
|
||||||
const initTime = Date.now();
|
const initTime = Date.now();
|
||||||
|
|
||||||
const request = args.request;
|
const request = args.request;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
// Todo only make available for get requests (loaders) and non api routes
|
const noSessionCookie = extractSessionCookieFromHeaders(request.headers) === null;
|
||||||
// use config
|
|
||||||
if (request.method !== 'GET' || !config.matcher.test(url.pathname)) {
|
if (!isPageRequest(request) || noSessionCookie) {
|
||||||
console.log('[Session]: Pathname ignored', url.pathname);
|
logger.log('Pathname ignored', url.pathname);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestMetadata: extractRequestMetadata(request),
|
requestMetadata: extractRequestMetadata(request),
|
||||||
session: null,
|
session: null,
|
||||||
@ -30,27 +35,28 @@ export async function getLoadContext(args: GetLoadContextArgs) {
|
|||||||
const splitUrl = url.pathname.split('/');
|
const splitUrl = url.pathname.split('/');
|
||||||
|
|
||||||
let team: TGetTeamByUrlResponse | null = null;
|
let team: TGetTeamByUrlResponse | null = null;
|
||||||
|
let teams: TGetTeamsResponse = [];
|
||||||
|
|
||||||
const session = await getSession(args.request);
|
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) {
|
if (session.isAuthenticated) {
|
||||||
// This is always loaded for the header.
|
let teamUrl = null;
|
||||||
teams = await getTeams({ userId: session.user.id });
|
|
||||||
|
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();
|
const endTime = Date.now();
|
||||||
console.log(`[Session]: Pathname accepted in ${endTime - initTime}ms`, url.pathname);
|
logger.log(`Pathname accepted in ${endTime - initTime}ms`, url.pathname);
|
||||||
|
|
||||||
// Todo: Optimise and chain promises.
|
|
||||||
// Todo: This is server only right?? Results not exposed?
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestMetadata: extractRequestMetadata(request),
|
requestMetadata: extractRequestMetadata(request),
|
||||||
@ -65,17 +71,20 @@ export async function getLoadContext(args: GetLoadContextArgs) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const isPageRequest = (request: Request) => {
|
||||||
* Route matcher configuration that excludes common non-route paths:
|
const url = new URL(request.url);
|
||||||
* - /api/* (API routes)
|
|
||||||
* - /assets/* (Static assets)
|
if (request.method !== 'GET') {
|
||||||
* - /build/* (Build output)
|
return false;
|
||||||
* - /favicon.* (Favicon files)
|
}
|
||||||
* - *.webmanifest (Web manifest files)
|
|
||||||
* - Paths starting with . (e.g. .well-known)
|
if (url.pathname.endsWith('.data')) {
|
||||||
*/
|
return true;
|
||||||
const config = {
|
}
|
||||||
matcher: new RegExp(
|
|
||||||
'/((?!api|assets|static|build|favicon|__manifest|site.webmanifest|manifest.webmanifest|\\..*).*)',
|
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 { API_V2_BETA_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
||||||
|
import { createTrpcContext } from '@documenso/trpc/server/context';
|
||||||
import { appRouter } from '@documenso/trpc/server/router';
|
import { appRouter } from '@documenso/trpc/server/router';
|
||||||
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
||||||
|
|
||||||
import { createHonoTrpcContext } from './trpc-context';
|
|
||||||
|
|
||||||
export const openApiTrpcServerHandler = async (c: Context) => {
|
export const openApiTrpcServerHandler = async (c: Context) => {
|
||||||
return createOpenApiFetchHandler<typeof appRouter>({
|
return createOpenApiFetchHandler<typeof appRouter>({
|
||||||
endpoint: API_V2_BETA_URL,
|
endpoint: API_V2_BETA_URL,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
// Todo: Test this, since it's not using the createContext params.
|
// 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,
|
req: c.req.raw,
|
||||||
onError: (opts) => handleTrpcRouterError(opts, 'apiV2'),
|
onError: (opts) => handleTrpcRouterError(opts, 'apiV2'),
|
||||||
// Not sure why we need to do this since we handle it in errorFormatter which runs after this.
|
// 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 { trpcServer } from '@hono/trpc-server';
|
||||||
|
|
||||||
|
import { createTrpcContext } from '@documenso/trpc/server/context';
|
||||||
import { appRouter } from '@documenso/trpc/server/router';
|
import { appRouter } from '@documenso/trpc/server/router';
|
||||||
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
||||||
|
|
||||||
import { createHonoTrpcContext } from './trpc-context';
|
|
||||||
|
|
||||||
// Todo
|
// Todo
|
||||||
// export const config = {
|
// export const config = {
|
||||||
// maxDuration: 120,
|
// maxDuration: 120,
|
||||||
@ -21,6 +20,6 @@ import { createHonoTrpcContext } from './trpc-context';
|
|||||||
export const reactRouterTrpcServer = trpcServer({
|
export const reactRouterTrpcServer = trpcServer({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
endpoint: '/api/trpc',
|
endpoint: '/api/trpc',
|
||||||
createContext: async (_, c) => createHonoTrpcContext({ c, requestSource: 'app' }),
|
createContext: async (_, c) => createTrpcContext({ c, requestSource: 'app' }),
|
||||||
onError: (opts) => handleTrpcRouterError(opts, 'trpc'),
|
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
2884
package-lock.json
generated
2884
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -3,13 +3,11 @@
|
|||||||
"version": "1.9.0-rc.11",
|
"version": "1.9.0-rc.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
"dev": "turbo run dev --filter=@documenso/web",
|
|
||||||
"dev:remix": "turbo run dev --filter=@documenso/remix",
|
"dev:remix": "turbo run dev --filter=@documenso/remix",
|
||||||
"dev:web": "turbo run dev --filter=@documenso/web",
|
|
||||||
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
||||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||||
"start": "turbo run start --filter=@documenso/web --filter=@documenso/documentation --filter=@documenso/openpage-api",
|
"start": "turbo run start --filter=@documenso/remix --filter=@documenso/documentation --filter=@documenso/openpage-api",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"lint:fix": "turbo run lint:fix",
|
"lint:fix": "turbo run lint:fix",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
||||||
@ -32,7 +30,7 @@
|
|||||||
"precommit": "npm install && git add package.json package-lock.json",
|
"precommit": "npm install && git add package.json package-lock.json",
|
||||||
"trigger:dev": "npm run with:env -- npx trigger-cli dev --handler-path=\"/api/jobs\"",
|
"trigger:dev": "npm run with:env -- npx trigger-cli dev --handler-path=\"/api/jobs\"",
|
||||||
"inngest:dev": "inngest dev -u http://localhost:3000/api/jobs",
|
"inngest:dev": "inngest dev -u http://localhost:3000/api/jobs",
|
||||||
"make:version": " npm version --workspace @documenso/web --include-workspace-root --no-git-tag-version -m \"v%s\"",
|
"make:version": "npm version --workspace @documenso/remix --include-workspace-root --no-git-tag-version -m \"v%s\"",
|
||||||
"translate": "npm run translate:extract && npm run translate:compile",
|
"translate": "npm run translate:extract && npm run translate:compile",
|
||||||
"translate:extract": "lingui extract --clean",
|
"translate:extract": "lingui extract --clean",
|
||||||
"translate:compile": "lingui compile"
|
"translate:compile": "lingui compile"
|
||||||
@ -40,7 +38,7 @@
|
|||||||
"packageManager": "npm@10.7.0",
|
"packageManager": "npm@10.7.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=10.7.0",
|
"npm": ">=10.7.0",
|
||||||
"node": ">=18.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
@ -68,11 +66,9 @@
|
|||||||
"@documenso/pdf-sign": "^0.1.0",
|
"@documenso/pdf-sign": "^0.1.0",
|
||||||
"@documenso/prisma": "^0.0.0",
|
"@documenso/prisma": "^0.0.0",
|
||||||
"@lingui/core": "^4.11.3",
|
"@lingui/core": "^4.11.3",
|
||||||
"@prisma/client": "^5.4.2",
|
|
||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
"next-runtime-env": "^3.2.0",
|
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import SwaggerUI from 'swagger-ui-react';
|
|
||||||
import 'swagger-ui-react/swagger-ui.css';
|
|
||||||
|
|
||||||
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
|
|
||||||
|
|
||||||
export const OpenApiDocsPage = () => {
|
|
||||||
// Todo
|
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const body = document.body;
|
|
||||||
|
|
||||||
if (resolvedTheme === 'dark') {
|
|
||||||
body.classList.add('swagger-dark-theme');
|
|
||||||
} else {
|
|
||||||
body.classList.remove('swagger-dark-theme');
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
body.classList.remove('swagger-dark-theme');
|
|
||||||
};
|
|
||||||
}, [resolvedTheme]);
|
|
||||||
|
|
||||||
return <SwaggerUI spec={OpenAPIV1} displayOperationId={true} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OpenApiDocsPage;
|
|
||||||
@ -1,17 +1,43 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
|
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
|
||||||
|
|
||||||
import { authDebugger } from '../utils/debugger';
|
import { appLog } from '@documenso/lib/utils/debugger';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
|
export const sessionCookieName = 'sessionId';
|
||||||
|
|
||||||
|
const getAuthSecret = () => {
|
||||||
|
const authSecret = env('NEXTAUTH_SECRET');
|
||||||
|
|
||||||
|
if (!authSecret) {
|
||||||
|
throw new Error('NEXTAUTH_SECRET is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return authSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {
|
||||||
|
const cookieHeader = headers.get('cookie') || '';
|
||||||
|
const cookiePairs = cookieHeader.split(';');
|
||||||
|
const sessionCookie = cookiePairs.find((pair) => pair.trim().startsWith(sessionCookieName));
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionCookie.split('=')[1].trim();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the session cookie attached to the request headers.
|
* Get the session cookie attached to the request headers.
|
||||||
*
|
*
|
||||||
* @param c - The Hono context.
|
* @param c - The Hono context.
|
||||||
|
* @returns The session ID or null if no session cookie is found.
|
||||||
*/
|
*/
|
||||||
export const getSessionCookie = async (c: Context) => {
|
export const getSessionCookie = async (c: Context): Promise<string | null> => {
|
||||||
const sessionId = await getSignedCookie(c, 'secret', 'sessionId'); // Todo: Use secret
|
const sessionId = await getSignedCookie(c, getAuthSecret(), sessionCookieName);
|
||||||
|
|
||||||
return sessionId;
|
return sessionId || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,13 +47,13 @@ export const getSessionCookie = async (c: Context) => {
|
|||||||
* @param sessionToken - The session token to set.
|
* @param sessionToken - The session token to set.
|
||||||
*/
|
*/
|
||||||
export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
||||||
await setSignedCookie(c, 'sessionId', sessionToken, 'secret', {
|
await setSignedCookie(c, sessionCookieName, sessionToken, getAuthSecret(), {
|
||||||
path: '/',
|
path: '/',
|
||||||
// sameSite: '', // whats the default? we need to change this for embed right?
|
// sameSite: '', // whats the default? we need to change this for embed right?
|
||||||
// secure: true,
|
// secure: true,
|
||||||
domain: 'localhost', // todo
|
domain: 'localhost', // todo
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
authDebugger(`Error setting signed cookie: ${err}`);
|
appLog('SetSessionCookie', `Error setting signed cookie: ${err}`);
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { env } from '@documenso/lib/utils/env';
|
|
||||||
|
|
||||||
// Todo: Delete
|
|
||||||
export const authDebugger = (message: string) => {
|
|
||||||
if (env('NODE_ENV') === 'development') {
|
|
||||||
// console.log(`[DEBUG]: ${message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -6,14 +6,11 @@ import { AuthenticationErrorCode } from '../errors/error-codes';
|
|||||||
import type { SessionValidationResult } from '../session/session';
|
import type { SessionValidationResult } from '../session/session';
|
||||||
import { validateSessionToken } from '../session/session';
|
import { validateSessionToken } from '../session/session';
|
||||||
import { getSessionCookie } from '../session/session-cookies';
|
import { getSessionCookie } from '../session/session-cookies';
|
||||||
import { authDebugger } from './debugger';
|
|
||||||
|
|
||||||
export const getSession = async (c: Context | Request): Promise<SessionValidationResult> => {
|
export const getSession = async (c: Context | Request): Promise<SessionValidationResult> => {
|
||||||
// Todo: Make better
|
// Todo: Make better
|
||||||
const sessionId = await getSessionCookie(mapRequestToContextForCookie(c));
|
const sessionId = await getSessionCookie(mapRequestToContextForCookie(c));
|
||||||
|
|
||||||
authDebugger(`Session ID: ${sessionId}`);
|
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return {
|
return {
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next-auth": "4.24.5",
|
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { MessageDescriptor } from '@lingui/core';
|
|||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { TeamMemberRole } from '@prisma/client';
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+$');
|
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
|
||||||
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
|
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
|
||||||
|
|
||||||
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
|
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
// export const isErrorCode = (code: unknown): code is ErrorCode => {
|
|
||||||
// return typeof code === 'string' && code in ErrorCode;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
||||||
|
|
||||||
// Todo: Delete file
|
|
||||||
// Todo: Delete file
|
|
||||||
// Todo: Delete file
|
|
||||||
// Todo: Delete file
|
|
||||||
// export const ErrorCode = {
|
|
||||||
// INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
|
|
||||||
// USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
|
|
||||||
// CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
|
|
||||||
// INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
|
|
||||||
// TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
|
|
||||||
// TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
|
|
||||||
// TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
|
|
||||||
// TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
|
|
||||||
// INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
|
|
||||||
// INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
|
|
||||||
// INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
|
|
||||||
// INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
|
||||||
// MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
|
||||||
// MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
|
||||||
// UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
|
|
||||||
// ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
|
|
||||||
// } as const;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { cache } from 'react';
|
|
||||||
|
|
||||||
import { getServerSession as getNextAuthServerSession } from 'next-auth';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { NEXT_AUTH_OPTIONS } from './auth-options';
|
|
||||||
|
|
||||||
export const getServerComponentSession = cache(async () => {
|
|
||||||
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
|
||||||
return { user: null, session: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
email: session.user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user.disabled) {
|
|
||||||
return { user: null, session: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, session };
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getRequiredServerComponentSession = cache(async () => {
|
|
||||||
const { user, session } = await getServerComponentSession();
|
|
||||||
|
|
||||||
if (!user || !session) {
|
|
||||||
throw new Error('No session found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, session };
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
|
|
||||||
import { getServerSession as getNextAuthServerSession } from 'next-auth';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { NEXT_AUTH_OPTIONS } from './auth-options';
|
|
||||||
|
|
||||||
export interface GetServerSessionOptions {
|
|
||||||
req: NextApiRequest | GetServerSidePropsContext['req'];
|
|
||||||
res: NextApiResponse | GetServerSidePropsContext['res'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSession = async ({ req, res }: GetServerSessionOptions) => {
|
|
||||||
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
|
||||||
return { user: null, session: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
email: session.user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { user, session };
|
|
||||||
};
|
|
||||||
@ -7,8 +7,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"client-only/",
|
"client-only/",
|
||||||
"server-only/",
|
"server-only/",
|
||||||
"universal/",
|
"universal/"
|
||||||
"next-auth/"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
@ -29,14 +28,12 @@
|
|||||||
"@lingui/core": "^4.11.3",
|
"@lingui/core": "^4.11.3",
|
||||||
"@lingui/macro": "^4.11.3",
|
"@lingui/macro": "^4.11.3",
|
||||||
"@lingui/react": "^4.11.3",
|
"@lingui/react": "^4.11.3",
|
||||||
"@next-auth/prisma-adapter": "1.0.7",
|
|
||||||
"@noble/ciphers": "0.4.0",
|
"@noble/ciphers": "0.4.0",
|
||||||
"@noble/hashes": "1.3.2",
|
"@noble/hashes": "1.3.2",
|
||||||
"@node-rs/bcrypt": "^1.10.0",
|
"@node-rs/bcrypt": "^1.10.0",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.3",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@trigger.dev/nextjs": "^2.3.18",
|
|
||||||
"@trigger.dev/sdk": "^2.3.18",
|
"@trigger.dev/sdk": "^2.3.18",
|
||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
@ -45,8 +42,6 @@
|
|||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "14.2.6",
|
|
||||||
"next-auth": "4.24.5",
|
|
||||||
"oslo": "^0.17.0",
|
"oslo": "^0.17.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
|||||||
19
packages/lib/utils/debugger.ts
Normal file
19
packages/lib/utils/debugger.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
|
export const appLog = (context: string, ...args: Parameters<typeof console.log>) => {
|
||||||
|
if (env('NEXT_DEBUG') === 'true') {
|
||||||
|
console.log(`[${context}]: ${args[0]}`, ...args.slice(1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AppLogger {
|
||||||
|
public context: string;
|
||||||
|
|
||||||
|
constructor(context: string) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(...args: Parameters<typeof console.log>) {
|
||||||
|
appLog(this.context, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.4.2",
|
"@prisma/client": "^5.4.2",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
"prisma": "5.4.2",
|
"prisma": "^5.4.2",
|
||||||
"prisma-extension-kysely": "^2.1.0",
|
"prisma-extension-kysely": "^2.1.0",
|
||||||
"ts-pattern": "^5.0.6"
|
"ts-pattern": "^5.0.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,49 +1,39 @@
|
|||||||
import type { User } from '@prisma/client';
|
import type { Context } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import type { CreateNextContextOptions, NextApiRequest } from './adapters/next';
|
type CreateTrpcContextOptions = {
|
||||||
|
c: Context;
|
||||||
type CreateTrpcContext = CreateNextContextOptions & {
|
requestSource: 'app' | 'apiV1' | 'apiV2';
|
||||||
requestSource: 'apiV1' | 'apiV2' | 'app';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Todo: Delete
|
|
||||||
*/
|
|
||||||
export const createTrpcContext = async ({
|
export const createTrpcContext = async ({
|
||||||
req,
|
c,
|
||||||
res,
|
|
||||||
requestSource,
|
requestSource,
|
||||||
}: Omit<CreateTrpcContext, 'info'>): Promise<TrpcContext> => {
|
}: CreateTrpcContextOptions): Promise<TrpcContext> => {
|
||||||
const { session, user } = await getServerSession({ req, res });
|
const { session, user } = await getSession(c);
|
||||||
|
|
||||||
|
const req = c.req.raw;
|
||||||
|
|
||||||
const metadata: ApiRequestMetadata = {
|
const metadata: ApiRequestMetadata = {
|
||||||
requestMetadata: extractNextApiRequestMetadata(req),
|
requestMetadata: extractRequestMetadata(req),
|
||||||
source: requestSource,
|
source: requestSource,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rawTeamId = req.headers.get('x-team-id') || undefined;
|
||||||
|
|
||||||
const teamId = z.coerce
|
const teamId = z.coerce
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.parse(req.headers['x-team-id']);
|
.parse(rawTeamId);
|
||||||
|
|
||||||
if (!session) {
|
if (!session || !user) {
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
user: null,
|
|
||||||
teamId,
|
|
||||||
req,
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
return {
|
||||||
session: null,
|
session: null,
|
||||||
user: null,
|
user: null,
|
||||||
@ -73,6 +63,6 @@ export type TrpcContext = (
|
|||||||
}
|
}
|
||||||
) & {
|
) & {
|
||||||
teamId: number | undefined;
|
teamId: number | undefined;
|
||||||
req: Request | NextApiRequest;
|
req: Request;
|
||||||
metadata: ApiRequestMetadata;
|
metadata: ApiRequestMetadata;
|
||||||
};
|
};
|
||||||
|
|||||||
11
packages/ui/components/client-only.tsx
Normal file
11
packages/ui/components/client-only.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const ClientOnly = async ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted ? children : null;
|
||||||
|
};
|
||||||
@ -1,24 +1,31 @@
|
|||||||
import dynamic from 'next/dynamic';
|
// Todo: Not sure if this actually makes it client-only.
|
||||||
|
import { Suspense, lazy } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { Await } from 'react-router';
|
||||||
|
|
||||||
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
|
const LoadingComponent = () => (
|
||||||
ssr: false,
|
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
||||||
loading: () => (
|
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||||
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
<p className="text-muted-foreground mt-4">
|
||||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
<Trans>Loading document...</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4">
|
export const LazyPDFViewerImport = lazy(async () => import('./pdf-viewer'));
|
||||||
<Trans>Loading document...</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
export const LazyPDFViewer = (props: React.ComponentProps<typeof LazyPDFViewerImport>) => (
|
||||||
* LazyPDFViewer variant with no loader.
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
*/
|
<Await resolve={LazyPDFViewerImport}>
|
||||||
export const LazyPDFViewerNoLoader = dynamic(async () => import('./pdf-viewer'), {
|
<LazyPDFViewerImport {...props} />
|
||||||
ssr: false,
|
</Await>
|
||||||
});
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LazyPDFViewerNoLoader = (props: React.ComponentProps<typeof LazyPDFViewer>) => (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyPDFViewerImport {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user