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

@ -47,6 +47,7 @@ export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogP
setIsUploadingFile(true);
try {
// Todo
// const { type, data } = await putPdfFile(file);
const formData = new FormData();
@ -56,7 +57,7 @@ export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogP
method: 'POST',
body: formData,
})
.then((res) => res.json())
.then(async (res) => await res.json())
.catch((e) => {
console.error('Upload failed:', e);
throw new AppError('UPLOAD_FAILED');

View File

@ -426,7 +426,7 @@ export const TemplateDirectLinkDialog = ({
await toggleTemplateDirectLink({
templateId: template.id,
enabled: isEnabled,
}).catch((e) => null);
}).catch(() => null);
onOpenChange(false);
}}

View File

@ -159,7 +159,7 @@ export function TemplateUseDialog({
method: 'POST',
body: formData,
})
.then((res) => res.json())
.then(async (res) => await res.json())
.catch((e) => {
console.error('Upload failed:', e);
throw new AppError('UPLOAD_FAILED');

View File

@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { authClient } from '@documenso/auth/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -44,10 +44,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
await forgotPassword({ email }).catch(() => null);
await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
await navigate('/check-email');
toast({
title: _(msg`Reset email sent`),
@ -58,8 +58,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
});
form.reset();
navigate('/check-email');
};
return (

View File

@ -270,6 +270,8 @@ export const SignInForm = ({
const onSignInWithOIDCClick = async () => {
try {
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 2000));
// await signIn('oidc', {
// callbackUrl,
// });

View File

@ -185,6 +185,7 @@ export const SignUpForm = ({
const onSignUpWithOIDCClick = async () => {
try {
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 2000));
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {

View File

@ -121,7 +121,7 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
void fetch(`/api/file?key=${file.key}`, {
method: 'GET',
})
.then((res) => res.json())
.then(async (res) => await res.json())
.then((data) => {
const objectUrl = URL.createObjectURL(new Blob([data.binaryData]));

View File

@ -39,7 +39,7 @@ export const DocumentSigningAuthAccount = ({
// // Todo: Redirect to signin like below
// }
navigate(`/signin#email=${email}`);
await navigate(`/signin#email=${email}`);
} catch {
setIsSigningOut(false);

View File

@ -31,7 +31,7 @@ export const DocumentSigningAuthPageView = ({
// Todo: Redirect false
await authClient.signOut();
navigate(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
await navigate(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -107,7 +107,7 @@ export const DocumentSigningForm = ({
timestamp: new Date().toISOString(),
});
redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${recipient.token}/complete`);
await navigate(redirectUrl ? redirectUrl : `/sign/${recipient.token}/complete`);
};
return (
@ -157,7 +157,7 @@ export const DocumentSigningForm = ({
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => navigate(-1)}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
@ -239,7 +239,7 @@ export const DocumentSigningForm = ({
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => navigate(-1)}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>

View File

@ -0,0 +1,41 @@
/**
* https://posthog.com/docs/advanced/proxy/remix
*/
import type { Route } from './+types/ingest.$';
const API_HOST = 'eu.i.posthog.com';
const ASSET_HOST = 'eu-assets.i.posthog.com';
const posthogProxy = async (request: Request) => {
const url = new URL(request.url);
const hostname = url.pathname.startsWith('/ingest/static/') ? ASSET_HOST : API_HOST;
const newUrl = new URL(url);
newUrl.protocol = 'https';
newUrl.hostname = hostname;
newUrl.port = '443';
newUrl.pathname = newUrl.pathname.replace(/^\/ingest/, '');
const headers = new Headers(request.headers);
headers.set('host', hostname);
const response = await fetch(newUrl, {
method: request.method,
headers,
body: request.body,
});
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
};
export async function loader({ request }: Route.LoaderArgs) {
return posthogProxy(request);
}
export async function action({ request }: Route.ActionArgs) {
return posthogProxy(request);
}

View File

@ -35,5 +35,5 @@ export const loader = ({ request }: Route.LoaderArgs) => {
return null;
}
return redirect(NEXT_PUBLIC_MARKETING_URL());
throw redirect(NEXT_PUBLIC_MARKETING_URL());
};

View File

@ -2,6 +2,7 @@ import { createCookieSessionStorage } from 'react-router';
import { createThemeSessionResolver } from 'remix-themes';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { env } from '@documenso/lib/utils/env';
const themeSessionStorage = createCookieSessionStorage({
cookie: {
@ -12,7 +13,7 @@ const themeSessionStorage = createCookieSessionStorage({
secrets: ['insecure-secret'], // Todo: Don't need secret
// Todo: Check this works on production.
// Set domain and secure only if in production
...(import.meta.env.PROD ? { domain: NEXT_PUBLIC_WEBAPP_URL(), secure: true } : {}),
...(env('NODE_ENV') === 'production' ? { domain: NEXT_PUBLIC_WEBAPP_URL(), secure: true } : {}),
},
});

View File

@ -13,7 +13,7 @@ declare module 'react-router' {
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
}
const logger = new AppLogger('[Context]');
const logger = new AppLogger('Context');
export async function getLoadContext(args: GetLoadContextArgs) {
const initTime = Date.now();

View File

@ -1,6 +1,7 @@
import type { ClientResponse } from 'hono/client';
import { hc } from 'hono/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import type { AuthAppType } from '../server';
@ -107,10 +108,6 @@ export class AuthClient {
};
}
// Todo: Env
// Todo: Remove in favor of AuthClient
// export const authClient = hc<AuthAppType>('http://localhost:3000/api/auth');
export const authClient = new AuthClient({
baseUrl: 'http://localhost:3000/api/auth',
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth`,
});

View File

@ -2,7 +2,6 @@ import { zValidator } from '@hono/zod-validator';
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { Hono } from 'hono';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
@ -11,11 +10,13 @@ import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
import { prisma } from '@documenso/prisma';
import { onAuthorize } from '../lib/utils/authorizer';
import { getRequiredSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context';
import { ZPasskeyAuthorizeSchema } from '../types/passkey';
export const passkeyRoute = new Hono<HonoAuthContext>()
/**
* Authorize endpoint.
*/
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
@ -43,7 +44,7 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
.catch(() => null);
if (!challengeToken) {
return null;
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
if (challengeToken.expiresAt < new Date()) {
@ -96,7 +97,7 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
},
});
return null;
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
await prisma.passkey.update({
@ -117,28 +118,29 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
},
200,
);
})
});
.post('/register', async (c) => {
const { user } = await getRequiredSession(c);
// Todo
// .post('/register', async (c) => {
// const { user } = await getRequiredSession(c);
//
})
// //
// })
.post(
'/pre-authenticate',
zValidator(
'json',
z.object({
code: z.string(),
}),
),
async (c) => {
//
// .post(
// '/pre-authenticate',
// zValidator(
// 'json',
// z.object({
// code: z.string(),
// }),
// ),
// async (c) => {
// //
return c.json({
success: true,
recoveryCodes: result.recoveryCodes,
});
},
);
// return c.json({
// success: true,
// recoveryCodes: result.recoveryCodes,
// });
// },
// );

View File

@ -1,5 +1,6 @@
import { match } from 'ts-pattern';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { stripe } from '@documenso/lib/server-only/stripe';
@ -17,9 +18,7 @@ type StripeWebhookResponse = {
export const stripeWebhookHandler = async (req: Request) => {
try {
// Todo
// const isBillingEnabled = await getFlag('app_billing');
const isBillingEnabled = true;
const isBillingEnabled = IS_BILLING_ENABLED();
const webhookSecret = env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET');

View File

@ -1,25 +0,0 @@
import { useMemo } from 'react';
import type { CombinedStylesKey } from '../../../ui/primitives/document-flow/add-fields';
import { combinedStyles } from '../../../ui/primitives/document-flow/field-item';
const defaultFieldItemStyles = {
borderClass: 'border-field-card-border',
activeBorderClass: 'border-field-card-border/80',
initialsBGClass: 'text-field-card-foreground/50 bg-slate-900/10',
fieldBackground: 'bg-field-card-background',
};
export const useFieldItemStyles = (color: CombinedStylesKey | null) => {
return useMemo(() => {
if (!color) return defaultFieldItemStyles;
const selectedColorVariant = combinedStyles[color];
return {
activeBorderClass: selectedColorVariant?.borderActive,
borderClass: selectedColorVariant?.border,
initialsBGClass: selectedColorVariant?.initialsBG,
fieldBackground: selectedColorVariant?.fieldBackground,
};
}, [color]);
};

View File

@ -1,93 +0,0 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import {
FEATURE_FLAG_POLL_INTERVAL,
LOCAL_FEATURE_FLAGS,
isFeatureFlagEnabled,
} from '@documenso/lib/constants/feature-flags';
import { getAllFlags } from '@documenso/lib/universal/get-feature-flag';
import type { TFeatureFlagValue } from './feature-flag.types';
export type FeatureFlagContextValue = {
getFlag: (_key: string) => TFeatureFlagValue;
};
export const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
export const useFeatureFlags = () => {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureFlags must be used within a FeatureFlagProvider');
}
return context;
};
export function FeatureFlagProvider({
children,
initialFlags,
}: {
children: React.ReactNode;
initialFlags: Record<string, TFeatureFlagValue>;
}) {
const [flags, setFlags] = useState(initialFlags);
const getFlag = useCallback(
(flag: string) => {
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS[flag] ?? true;
}
return flags[flag] ?? false;
},
[flags],
);
/**
* Refresh the flags every `FEATURE_FLAG_POLL_INTERVAL` amount of time if the window is focused.
*/
useEffect(() => {
if (!isFeatureFlagEnabled()) {
return;
}
const interval = setInterval(() => {
if (document.hasFocus()) {
void getAllFlags().then((newFlags) => setFlags(newFlags));
}
}, FEATURE_FLAG_POLL_INTERVAL);
return () => {
clearInterval(interval);
};
}, []);
/**
* Refresh the flags when the window is focused.
*/
useEffect(() => {
if (!isFeatureFlagEnabled()) {
return;
}
const onFocus = () => void getAllFlags().then((newFlags) => setFlags(newFlags));
window.addEventListener('focus', onFocus);
return () => {
window.removeEventListener('focus', onFocus);
};
}, []);
return (
<FeatureFlagContext.Provider
value={{
getFlag,
}}
>
{children}
</FeatureFlagContext.Provider>
);
}

View File

@ -1,10 +0,0 @@
import { z } from 'zod';
export const ZFeatureFlagValueSchema = z.union([
z.boolean(),
z.string(),
z.number(),
z.undefined(),
]);
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;

View File

@ -16,8 +16,9 @@ export async function loadCatalog(lang: SupportedLanguages): Promise<{
}> {
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
// Todo
const { messages } = await import(`../../translations/${lang}/web.po`);
// const { messages } = await import(`../../translations/${lang}/web.${extension}`);
const messages = {};
return {
[lang]: messages,

View File

@ -1,6 +1,8 @@
export const DOCUMENSO_ENCRYPTION_KEY = '12345678912345678912345678912457';
import { env } from '../utils/env';
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = '12345678912345678912345678912458';
export const DOCUMENSO_ENCRYPTION_KEY = env('NEXT_PRIVATE_ENCRYPTION_KEY');
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = env('NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY');
if (typeof window === 'undefined') {
if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {

View File

@ -1,519 +0,0 @@
/// <reference types="../types/next-auth.d.ts" />
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { compare } from '@node-rs/bcrypt';
import { Prisma } from '@prisma/client';
import { IdentityProvider, UserSecurityAuditLogType } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { DateTime } from 'luxon';
import type { AuthOptions, Session, User } from 'next-auth';
import type { JWT } from 'next-auth/jwt';
import CredentialsProvider from 'next-auth/providers/credentials';
import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
import { AppError, AppErrorCode } from '../errors/app-error';
import { jobsClient } from '../jobs/client';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { decryptSecondaryData } from '../server-only/crypto/decrypt';
import { getMostRecentVerificationTokenByUserId } from '../server-only/user/get-most-recent-verification-token-by-user-id';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import type { TAuthenticationResponseJSONSchema } from '../types/webauthn';
import { ZAuthenticationResponseJSONSchema } from '../types/webauthn';
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
import { getAuthenticatorOptions } from '../utils/authenticator';
import { ErrorCode } from './error-codes';
// Delete unrecognized fields from authorization response to comply with
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
const prismaAdapter = PrismaAdapter(prisma);
const unsafe_linkAccount = prismaAdapter.linkAccount!;
const unsafe_accountModel = Prisma.dmmf.datamodel.models.find(({ name }) => name === 'Account');
if (!unsafe_accountModel) {
throw new Error('Account model not found');
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
prismaAdapter.linkAccount = (data) => {
const availableFields = unsafe_accountModel.fields.map((field) => field.name);
const newData = Object.keys(data).reduce(
(acc, key) => {
if (availableFields.includes(key)) {
acc[key] = data[key];
}
return acc;
},
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
{} as typeof data,
);
return unsafe_linkAccount(newData);
};
export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: prismaAdapter,
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
totpCode: {
label: 'Two-factor Code',
type: 'input',
placeholder: 'Code from authenticator app',
},
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
},
authorize: async (credentials, req) => {
if (!credentials) {
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
}
const { email, password, backupCode, totpCode } = credentials;
const user = await getUserByEmail({ email }).catch(() => {
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
});
if (!user.password) {
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
}
const isPasswordsSame = await compare(password, user.password);
const requestMetadata = extractNextAuthRequestMetadata(req);
if (!isPasswordsSame) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
},
});
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
}
const is2faEnabled = isTwoFactorAuthenticationEnabled({ user });
if (is2faEnabled) {
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
if (!isValid) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_2FA_FAIL,
},
});
throw new Error(
totpCode
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
: ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE,
);
}
}
if (!user.emailVerified) {
const mostRecentToken = await getMostRecentVerificationTokenByUserId({
userId: user.id,
});
if (
!mostRecentToken ||
mostRecentToken.expires.valueOf() <= Date.now() ||
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
) {
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
}
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
}
if (user.disabled) {
throw new Error(ErrorCode.ACCOUNT_DISABLED);
}
return {
id: Number(user.id),
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
} satisfies User;
},
}),
GoogleProvider<GoogleProfile>({
clientId: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID ?? '',
clientSecret: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET ?? '',
allowDangerousEmailAccountLinking: true,
profile(profile) {
return {
id: Number(profile.sub),
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
email: profile.email,
emailVerified: profile.email_verified ? new Date().toISOString() : null,
};
},
}),
{
id: 'oidc',
name: 'OIDC',
type: 'oauth',
wellKnown: process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN,
clientId: process.env.NEXT_PRIVATE_OIDC_CLIENT_ID,
clientSecret: process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
authorization: { params: { scope: 'openid email profile' } },
checks: ['pkce', 'state'],
idToken: true,
allowDangerousEmailAccountLinking: true,
profile(profile) {
return {
id: profile.sub,
email: profile.email || profile.preferred_username,
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
emailVerified:
process.env.NEXT_PRIVATE_OIDC_SKIP_VERIFY === 'true' || profile.email_verified
? new Date().toISOString()
: null,
};
},
},
CredentialsProvider({
id: 'webauthn',
name: 'Keypass',
credentials: {
csrfToken: { label: 'csrfToken', type: 'csrfToken' },
},
async authorize(credentials, req) {
const csrfToken = credentials?.csrfToken;
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
try {
const parsedBodyCredential = JSON.parse(req.body?.credential);
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
} catch {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const challengeToken = await prisma.anonymousVerificationToken
.delete({
where: {
id: csrfToken,
},
})
.catch(() => null);
if (!challengeToken) {
return null;
}
if (challengeToken.expiresAt < new Date()) {
throw new AppError(AppErrorCode.EXPIRED_CODE);
}
const passkey = await prisma.passkey.findFirst({
where: {
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
},
include: {
user: {
select: {
id: true,
email: true,
name: true,
emailVerified: true,
},
},
},
});
if (!passkey) {
throw new AppError(AppErrorCode.NOT_SETUP);
}
const user = passkey.user;
const { rpId, origin } = getAuthenticatorOptions();
const verification = await verifyAuthenticationResponse({
response: requestBodyCrediential,
expectedChallenge: challengeToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
authenticator: {
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null);
const requestMetadata = extractNextAuthRequestMetadata(req);
if (!verification?.verified) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
},
});
return null;
}
await prisma.passkey.update({
where: {
id: passkey.id,
},
data: {
lastUsedAt: new Date(),
counter: verification.authenticationInfo.newCounter,
},
});
return {
id: Number(user.id),
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
} satisfies User;
},
}),
CredentialsProvider({
id: 'manual',
name: 'Manual',
credentials: {
credential: { label: 'Credential', type: 'credential' },
},
async authorize(credentials, req) {
const credential = credentials?.credential;
if (typeof credential !== 'string' || credential.length === 0) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const decryptedCredential = decryptSecondaryData(credential);
if (!decryptedCredential) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const parsedCredential = JSON.parse(decryptedCredential);
if (typeof parsedCredential !== 'object' || parsedCredential === null) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const { userId, email } = parsedCredential;
if (typeof userId !== 'number' || typeof email !== 'string') {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
return {
id: Number(user.id),
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
} satisfies User;
},
}),
],
callbacks: {
async jwt({ token, user, trigger, account }) {
const merged = {
...token,
...user,
emailVerified: user?.emailVerified ? new Date(user.emailVerified).toISOString() : null,
} satisfies JWT;
if (!merged.email || typeof merged.emailVerified !== 'string') {
const userId = Number(merged.id ?? token.sub);
const retrieved = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!retrieved) {
return token;
}
merged.id = retrieved.id;
merged.name = retrieved.name;
merged.email = retrieved.email;
merged.emailVerified = retrieved.emailVerified?.toISOString() ?? null;
}
if (
merged.id &&
(!merged.lastSignedIn ||
DateTime.fromISO(merged.lastSignedIn).plus({ hours: 1 }) <= DateTime.now())
) {
merged.lastSignedIn = new Date().toISOString();
const user = await prisma.user.update({
where: {
id: Number(merged.id),
},
data: {
lastSignedIn: merged.lastSignedIn,
},
});
merged.emailVerified = user.emailVerified?.toISOString() ?? null;
}
if ((trigger === 'signIn' || trigger === 'signUp') && account?.provider === 'google') {
merged.emailVerified = user?.emailVerified
? new Date(user.emailVerified).toISOString()
: new Date().toISOString();
await prisma.user.update({
where: {
id: Number(merged.id),
},
data: {
emailVerified: merged.emailVerified,
identityProvider: IdentityProvider.GOOGLE,
},
});
}
return {
id: merged.id,
name: merged.name,
email: merged.email,
lastSignedIn: merged.lastSignedIn,
emailVerified: merged.emailVerified,
} satisfies JWT;
},
session({ token, session }) {
if (token && token.email) {
return {
...session,
user: {
id: Number(token.id),
name: token.name,
email: token.email,
emailVerified: token.emailVerified ?? null,
},
} satisfies Session;
}
return session;
},
async signIn({ user }) {
// This statement appears above so we can stil allow `oidc` connections
// while other signups are disabled.
if (env('NEXT_PRIVATE_OIDC_ALLOW_SIGNUP') === 'true') {
return true;
}
// We do this to stop OAuth providers from creating an account
// when signups are disabled
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
const userData = await getUserByEmail({ email: user.email! });
return !!userData;
}
return true;
},
},
cookies: {
sessionToken: {
name: formatSecureCookieName('next-auth.session-token'),
options: {
httpOnly: true,
sameSite: useSecureCookies ? 'none' : 'lax',
path: '/',
secure: useSecureCookies,
},
},
callbackUrl: {
name: formatSecureCookieName('next-auth.callback-url'),
options: {
sameSite: useSecureCookies ? 'none' : 'lax',
path: '/',
secure: useSecureCookies,
},
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: formatSecureCookieName('next-auth.csrf-token'),
options: {
httpOnly: true,
sameSite: useSecureCookies ? 'none' : 'lax',
path: '/',
secure: useSecureCookies,
},
},
pkceCodeVerifier: {
name: formatSecureCookieName('next-auth.pkce.code_verifier'),
options: {
httpOnly: true,
sameSite: useSecureCookies ? 'none' : 'lax',
path: '/',
secure: useSecureCookies,
},
},
state: {
name: formatSecureCookieName('next-auth.state'),
options: {
httpOnly: true,
sameSite: useSecureCookies ? 'none' : 'lax',
path: '/',
secure: useSecureCookies,
},
},
},
// Note: `events` are handled in `apps/web/src/pages/api/auth/[...nextauth].ts` to allow access to the request.
};

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',
});
}

View File

@ -1,111 +0,0 @@
import { z } from 'zod';
import type { TFeatureFlagValue } from '@documenso/lib/client-only/providers/feature-flag.types';
import { ZFeatureFlagValueSchema } from '@documenso/lib/client-only/providers/feature-flag.types';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
/**
* Evaluate whether a flag is enabled for the current user.
*
* @param flag The flag to evaluate.
* @param options See `GetFlagOptions`.
* @returns Whether the flag is enabled, or the variant value of the flag.
*/
export const getFlag = async (
flag: string,
options?: GetFlagOptions,
): Promise<TFeatureFlagValue> => {
const requestHeaders = options?.requestHeaders ?? {};
delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS[flag] ?? true;
}
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
return await fetch(url, {
headers: {
...requestHeaders,
},
next: {
revalidate: 60,
},
})
.then(async (res) => res.json())
.then((res) => ZFeatureFlagValueSchema.parse(res))
.catch((err) => {
console.error(err);
return LOCAL_FEATURE_FLAGS[flag] ?? false;
});
};
/**
* Get all feature flags for the current user if possible.
*
* @param options See `GetFlagOptions`.
* @returns A record of flags and their values for the user derived from the headers.
*/
export const getAllFlags = async (
options?: GetFlagOptions,
): Promise<Record<string, TFeatureFlagValue>> => {
const requestHeaders = options?.requestHeaders ?? {};
delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
return fetch(url, {
headers: {
...requestHeaders,
},
next: {
revalidate: 60,
},
})
.then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
.catch((err) => {
console.error(err);
return LOCAL_FEATURE_FLAGS;
});
};
/**
* Get all feature flags for anonymous users.
*
* @returns A record of flags and their values.
*/
export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFlagValue>> => {
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
return fetch(url, {
next: {
revalidate: 60,
},
})
.then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
.catch((err) => {
console.error(err);
return LOCAL_FEATURE_FLAGS;
});
};
interface GetFlagOptions {
/**
* The headers to attach to the request to evaluate flags.
*
* The authenticated user will be derived from the headers if possible.
*/
requestHeaders: Record<string, string>;
}

View File

@ -5,7 +5,6 @@ import {
S3Client,
} from '@aws-sdk/client-s3';
import slugify from '@sindresorhus/slugify';
import { type JWT } from 'next-auth/jwt';
import path from 'node:path';
import { env } from '@documenso/lib/utils/env';
@ -19,7 +18,7 @@ export const getPresignPostUrl = async (fileName: string, contentType: string) =
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
const token: JWT | null = null;
const token: { id: string } | null = null;
try {
const baseUrl = NEXT_PUBLIC_WEBAPP_URL();

View File

@ -1,9 +1,7 @@
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));
}
// if (env('NEXT_DEBUG') === 'true') {
console.log(`[${context}]: ${args[0]}`, ...args.slice(1));
// }
};
export class AppLogger {

View File

@ -2,7 +2,7 @@
type EnvironmentVariable = keyof NodeJS.ProcessEnv;
export const env = (variable: EnvironmentVariable | (string & {})): string | undefined => {
export const env = (variable: EnvironmentVariable | (string & object)): string | undefined => {
// console.log({
// ['typeof window']: typeof window,
// ['process.env']: process.env,

View File

@ -17,18 +17,18 @@ export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOpti
throw new Error('No certificate path provided for Google Cloud HSM signing');
}
const googleApplicationCredentials = env('GOOGLE_APPLICATION_CREDENTIALS');
const googleApplicationCredentialsContents = env(
'NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS',
);
// To handle hosting in serverless environments like Vercel we can supply the base64 encoded
// application credentials as an environment variable and write it to a file if it doesn't exist
if (
env('GOOGLE_APPLICATION_CREDENTIALS') &&
env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS')
) {
if (!fs.existsSync(env('GOOGLE_APPLICATION_CREDENTIALS'))) {
const contents = new Uint8Array(
Buffer.from(env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS'), 'base64'),
);
if (googleApplicationCredentials && googleApplicationCredentialsContents) {
if (!fs.existsSync(googleApplicationCredentials)) {
const contents = new Uint8Array(Buffer.from(googleApplicationCredentialsContents, 'base64'));
fs.writeFileSync(env('GOOGLE_APPLICATION_CREDENTIALS'), contents);
fs.writeFileSync(googleApplicationCredentials, contents);
}
}
@ -45,8 +45,12 @@ export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOpti
let cert: Buffer | null = null;
if (env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS')) {
cert = Buffer.from(env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS'), 'base64');
const googleCloudHsmPublicCrtFileContents = env(
'NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS',
);
if (googleCloudHsmPublicCrtFileContents) {
cert = Buffer.from(googleCloudHsmPublicCrtFileContents, 'base64');
}
if (!cert) {

View File

@ -24,8 +24,10 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
let cert: Buffer | null = null;
if (env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS')) {
cert = Buffer.from(env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS'), 'base64');
const localFileContents = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS');
if (localFileContents) {
cert = Buffer.from(localFileContents, 'base64');
}
if (!cert) {

View File

@ -4,7 +4,7 @@ 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 { User } from '@documenso/prisma/client';
import type { Session, User } from '@documenso/prisma/client';
type CreateTrpcContextOptions = {
c: Context;
@ -58,7 +58,7 @@ export type TrpcContext = (
user: null;
}
| {
session: unknown;
session: Session;
user: User;
}
) & {

View File

@ -66,7 +66,7 @@ const t = initTRPC
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
const authorizationHeader = ctx.req.headers.authorization;
const authorizationHeader = ctx.req.headers.get('authorization');
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
if (authorizationHeader) {

View File

@ -22,7 +22,7 @@ type ComboBoxOption<T = OptionValue> = {
};
type MultiSelectComboboxProps<T = OptionValue> = {
emptySelectionPlaceholder?: React.ReactNode | string;
emptySelectionPlaceholder?: React.ReactElement | string;
enableClearAllButton?: boolean;
loading?: boolean;
inputPlaceholder?: MessageDescriptor;