This commit is contained in:
David Nguyen
2025-02-05 23:37:21 +11:00
parent 9c7910a070
commit 7effe66387
26 changed files with 1518 additions and 1951 deletions

View File

@ -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;

View File

@ -1,17 +1,43 @@
import type { Context } from 'hono';
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.
*
* @param c - The Hono context.
* @returns The session ID or null if no session cookie is found.
*/
export const getSessionCookie = async (c: Context) => {
const sessionId = await getSignedCookie(c, 'secret', 'sessionId'); // Todo: Use secret
export const getSessionCookie = async (c: Context): Promise<string | null> => {
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.
*/
export const setSessionCookie = async (c: Context, sessionToken: string) => {
await setSignedCookie(c, 'sessionId', sessionToken, 'secret', {
await setSignedCookie(c, sessionCookieName, sessionToken, getAuthSecret(), {
path: '/',
// sameSite: '', // whats the default? we need to change this for embed right?
// secure: true,
domain: 'localhost', // todo
}).catch((err) => {
authDebugger(`Error setting signed cookie: ${err}`);
appLog('SetSessionCookie', `Error setting signed cookie: ${err}`);
throw err;
});

View File

@ -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}`);
}
};

View File

@ -6,14 +6,11 @@ import { AuthenticationErrorCode } from '../errors/error-codes';
import type { SessionValidationResult } from '../session/session';
import { validateSessionToken } from '../session/session';
import { getSessionCookie } from '../session/session-cookies';
import { authDebugger } from './debugger';
export const getSession = async (c: Context | Request): Promise<SessionValidationResult> => {
// Todo: Make better
const sessionId = await getSessionCookie(mapRequestToContextForCookie(c));
authDebugger(`Session ID: ${sessionId}`);
if (!sessionId) {
return {
isAuthenticated: false,

View File

@ -17,7 +17,6 @@
"@documenso/prisma": "*",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next-auth": "4.24.5",
"react": "^18",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"

View File

@ -2,7 +2,7 @@ import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
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_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {

View File

@ -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;

View File

@ -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 };
});

View File

@ -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 };
};

View File

@ -7,8 +7,7 @@
"files": [
"client-only/",
"server-only/",
"universal/",
"next-auth/"
"universal/"
],
"scripts": {
"lint": "eslint .",
@ -29,14 +28,12 @@
"@lingui/core": "^4.11.3",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",
"@next-auth/prisma-adapter": "1.0.7",
"@noble/ciphers": "0.4.0",
"@noble/hashes": "1.3.2",
"@node-rs/bcrypt": "^1.10.0",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3",
"@sindresorhus/slugify": "^2.2.1",
"@trigger.dev/nextjs": "^2.3.18",
"@trigger.dev/sdk": "^2.3.18",
"@upstash/redis": "^1.20.6",
"@vvo/tzdb": "^6.117.0",
@ -45,8 +42,6 @@
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
"next": "14.2.6",
"next-auth": "4.24.5",
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"pg": "^8.11.3",
@ -63,4 +58,4 @@
"@types/luxon": "^3.3.1",
"@types/pg": "^8.11.4"
}
}
}

View 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);
}
}

View File

@ -23,7 +23,7 @@
"dependencies": {
"@prisma/client": "^5.4.2",
"kysely": "^0.27.3",
"prisma": "5.4.2",
"prisma": "^5.4.2",
"prisma-extension-kysely": "^2.1.0",
"ts-pattern": "^5.0.6"
},
@ -36,4 +36,4 @@
"typescript": "5.6.2",
"zod-prisma-types": "3.1.9"
}
}
}

View File

@ -1,49 +1,39 @@
import type { User } from '@prisma/client';
import type { Context } from 'hono';
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 { 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 CreateTrpcContext = CreateNextContextOptions & {
requestSource: 'apiV1' | 'apiV2' | 'app';
type CreateTrpcContextOptions = {
c: Context;
requestSource: 'app' | 'apiV1' | 'apiV2';
};
/**
* Todo: Delete
*/
export const createTrpcContext = async ({
req,
res,
c,
requestSource,
}: Omit<CreateTrpcContext, 'info'>): Promise<TrpcContext> => {
const { session, user } = await getServerSession({ req, res });
}: CreateTrpcContextOptions): Promise<TrpcContext> => {
const { session, user } = await getSession(c);
const req = c.req.raw;
const metadata: ApiRequestMetadata = {
requestMetadata: extractNextApiRequestMetadata(req),
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(req.headers['x-team-id']);
.parse(rawTeamId);
if (!session) {
return {
session: null,
user: null,
teamId,
req,
metadata,
};
}
if (!user) {
if (!session || !user) {
return {
session: null,
user: null,
@ -73,6 +63,6 @@ export type TrpcContext = (
}
) & {
teamId: number | undefined;
req: Request | NextApiRequest;
req: Request;
metadata: ApiRequestMetadata;
};

View 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;
};

View File

@ -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 { Loader } from 'lucide-react';
import { Await } from 'react-router';
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
ssr: false,
loading: () => (
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" />
const LoadingComponent = () => (
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</div>
);
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</div>
),
});
export const LazyPDFViewerImport = lazy(async () => import('./pdf-viewer'));
/**
* LazyPDFViewer variant with no loader.
*/
export const LazyPDFViewerNoLoader = dynamic(async () => import('./pdf-viewer'), {
ssr: false,
});
export const LazyPDFViewer = (props: React.ComponentProps<typeof LazyPDFViewerImport>) => (
<Suspense fallback={<LoadingComponent />}>
<Await resolve={LazyPDFViewerImport}>
<LazyPDFViewerImport {...props} />
</Await>
</Suspense>
);
export const LazyPDFViewerNoLoader = (props: React.ComponentProps<typeof LazyPDFViewer>) => (
<Suspense fallback={null}>
<LazyPDFViewerImport {...props} />
</Suspense>
);