fix: errors

This commit is contained in:
David Nguyen
2025-02-06 01:57:23 +11:00
parent 7effe66387
commit 738201eb55
43 changed files with 159 additions and 1068 deletions

View File

@ -13,6 +13,10 @@ const ZBackupCodeSchema = z.array(z.string());
export const getBackupCodes = ({ user }: GetBackupCodesOptions) => {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
if (!user.twoFactorEnabled) {
throw new Error('User has not enabled 2FA');
}

View File

@ -7,7 +7,10 @@ import { verifyBackupCode } from './verify-backup-code';
type ValidateTwoFactorAuthenticationOptions = {
totpCode?: string;
backupCode?: string;
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
user: Pick<
User,
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
>;
};
export const validateTwoFactorAuthentication = async ({
@ -28,7 +31,7 @@ export const validateTwoFactorAuthentication = async ({
}
if (backupCode) {
return await verifyBackupCode({ user, backupCode });
return verifyBackupCode({ user, backupCode });
}
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');

View File

@ -6,7 +6,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricDecrypt } from '../../universal/crypto';
type VerifyTwoFactorAuthenticationTokenOptions = {
user: User;
user: Pick<User, 'id' | 'twoFactorSecret'>;
totpCode: string;
// The number of windows to look back
window?: number;
@ -22,6 +22,10 @@ export const verifyTwoFactorAuthenticationToken = async ({
}: VerifyTwoFactorAuthenticationTokenOptions) => {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
if (!user.twoFactorSecret) {
throw new Error('user missing 2fa secret');
}

View File

@ -3,12 +3,12 @@ import type { User } from '@prisma/client';
import { getBackupCodes } from './get-backup-code';
type VerifyBackupCodeParams = {
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
user: Pick<User, 'id' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
backupCode: string;
};
export const verifyBackupCode = async ({ user, backupCode }: VerifyBackupCodeParams) => {
const userBackupCodes = await getBackupCodes({ user });
export const verifyBackupCode = ({ user, backupCode }: VerifyBackupCodeParams) => {
const userBackupCodes = getBackupCodes({ user });
if (!userBackupCodes) {
throw new Error('User has no backup codes');

View File

@ -1,56 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
/**
* Get all the evaluated feature flags based on the current user if possible.
*/
export default async function handlerFeatureFlagAll(req: Request) {
const requestHeaders = Object.fromEntries(req.headers.entries());
const nextReq = new NextRequest(req, {
headers: requestHeaders,
});
const token = await getToken({ req: nextReq });
const postHog = PostHogServerClient();
// Return the local feature flags if PostHog is not enabled, true by default.
// The front end should not call this API if PostHog is not enabled to reduce network requests.
if (!postHog) {
return NextResponse.json(LOCAL_FEATURE_FLAGS);
}
const distinctId = extractDistinctUserId(token, nextReq);
const featureFlags = await postHog.getAllFlags(distinctId, mapJwtToFlagProperties(token));
const res = NextResponse.json(featureFlags);
res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
const origin = req.headers.get('origin');
if (origin) {
if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}
return res;
}

View File

@ -1,26 +0,0 @@
import { headers } from 'next/headers';
import { getAllFlags, getFlag } from '@documenso/lib/universal/get-feature-flag';
/**
* Evaluate whether a flag is enabled for the current user in a server component.
*
* @param flag The flag to evaluate.
* @returns Whether the flag is enabled, or the variant value of the flag.
*/
export const getServerComponentFlag = async (flag: string) => {
return await getFlag(flag, {
requestHeaders: Object.fromEntries(headers().entries()),
});
};
/**
* Get all feature flags for the current user from a server component.
*
* @returns A record of flags and their values for the user derived from the headers.
*/
export const getServerComponentAllFlags = async () => {
return await getAllFlags({
requestHeaders: Object.fromEntries(headers().entries()),
});
};

View File

@ -1,136 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { nanoid } from 'nanoid';
import type { JWT } from 'next-auth/jwt';
import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
/**
* Evaluate a single feature flag based on the current user if possible.
*
* @param req The request with a query parameter `flag`. Example request URL: /api/feature-flag/get?flag=flag-name
* @returns A Response with the feature flag value.
*/
export default async function handleFeatureFlagGet(req: Request) {
const { searchParams } = new URL(req.url ?? '');
const flag = searchParams.get('flag');
const requestHeaders = Object.fromEntries(req.headers.entries());
const nextReq = new NextRequest(req, {
headers: requestHeaders,
});
const token = await getToken({ req: nextReq });
if (!flag) {
return NextResponse.json(
{
error: 'Missing flag query parameter.',
},
{
status: 400,
headers: {
'content-type': 'application/json',
},
},
);
}
const postHog = PostHogServerClient();
// Return the local feature flags if PostHog is not enabled, true by default.
// The front end should not call this API if PostHog is disabled to reduce network requests.
if (!postHog) {
return NextResponse.json(LOCAL_FEATURE_FLAGS[flag] ?? true);
}
const distinctId = extractDistinctUserId(token, nextReq);
const featureFlag = await postHog.getFeatureFlag(flag, distinctId, mapJwtToFlagProperties(token));
const res = NextResponse.json(featureFlag);
res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
const origin = req.headers.get('Origin');
if (origin) {
if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}
return res;
}
/**
* Map a JWT to properties which are consumed by PostHog to evaluate feature flags.
*
* @param jwt The JWT of the current user.
* @returns A map of properties which are consumed by PostHog.
*/
export const mapJwtToFlagProperties = (
jwt?: JWT | null,
): {
groups?: Record<string, string>;
personProperties?: Record<string, string>;
groupProperties?: Record<string, Record<string, string>>;
} => {
return {
personProperties: {
email: jwt?.email ?? '',
},
groupProperties: {
// Add properties to group users into different groups, such as billing plan.
},
};
};
/**
* Extract a distinct ID from a JWT and request.
*
* Will fallback to a random ID if no ID could be extracted from either the JWT or request.
*
* @param jwt The JWT of the current user.
* @param request Request potentially containing a PostHog `distinct_id` cookie.
* @returns A distinct user ID.
*/
export const extractDistinctUserId = (jwt: JWT | null, request: NextRequest): string => {
const config = extractPostHogConfig();
const email = jwt?.email;
const userId = jwt?.id?.toString();
let fallbackDistinctId = nanoid();
if (config) {
try {
const postHogCookie = JSON.parse(
request.cookies.get(`ph_${config.key}_posthog`)?.value ?? '',
);
const postHogDistinctId = postHogCookie['distinct_id'];
if (typeof postHogDistinctId === 'string') {
fallbackDistinctId = postHogDistinctId;
}
} catch {
// Do nothing.
}
}
return email ?? userId ?? fallbackDistinctId;
};

View File

@ -22,10 +22,12 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
let browser: Browser;
if (env('NEXT_PRIVATE_BROWSERLESS_URL')) {
const browserlessUrl = env('NEXT_PRIVATE_BROWSERLESS_URL');
if (browserlessUrl) {
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
browser = await chromium.connectOverCDP(env('NEXT_PRIVATE_BROWSERLESS_URL'));
browser = await chromium.connectOverCDP(browserlessUrl);
} else {
browser = await chromium.launch();
}

View File

@ -28,9 +28,16 @@ import {
import { env } from '../../utils/env';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
const fontCaveat = await fetch(env('FONT_CAVEAT_URI')).then(async (res) => res.arrayBuffer());
const fontCaveatUri = env('FONT_CAVEAT_URI');
const fontNotoSansUri = env('FONT_NOTO_SANS_URI');
const fontNoto = await fetch(env('FONT_NOTO_SANS_URI')).then(async (res) => res.arrayBuffer());
if (!fontCaveatUri || !fontNotoSansUri) {
throw new Error('Missing font URI');
}
const fontCaveat = await fetch(fontCaveatUri).then(async (res) => res.arrayBuffer());
const fontNoto = await fetch(fontNotoSansUri).then(async (res) => res.arrayBuffer());
const isSignatureField = isSignatureFieldType(field.type);

View File

@ -1,12 +1,11 @@
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import type { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
import { ONE_DAY } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
export const forgotPassword = async ({ email }: { email: string }) => {
const user = await prisma.user.findFirst({
where: {
email: {

View File

@ -59,7 +59,7 @@ export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileO
});
if (isUrlTakenByAnotherUser || isUrlTakenByAnotherTeam) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
throw new AppError('PROFILE_URL_TAKEN', {
message: 'The profile username is already taken',
});
}