diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx
index 276262273..a69ffec76 100644
--- a/apps/remix/app/components/dialogs/template-create-dialog.tsx
+++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx
@@ -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');
diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
index ca92adb81..1345e7803 100644
--- a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
+++ b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
@@ -426,7 +426,7 @@ export const TemplateDirectLinkDialog = ({
await toggleTemplateDirectLink({
templateId: template.id,
enabled: isEnabled,
- }).catch((e) => null);
+ }).catch(() => null);
onOpenChange(false);
}}
diff --git a/apps/remix/app/components/dialogs/template-use-dialog.tsx b/apps/remix/app/components/dialogs/template-use-dialog.tsx
index 7302a06cf..40f8a73f4 100644
--- a/apps/remix/app/components/dialogs/template-use-dialog.tsx
+++ b/apps/remix/app/components/dialogs/template-use-dialog.tsx
@@ -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');
diff --git a/apps/remix/app/components/forms/forgot-password.tsx b/apps/remix/app/components/forms/forgot-password.tsx
index 09f99622c..82cfab41d 100644
--- a/apps/remix/app/components/forms/forgot-password.tsx
+++ b/apps/remix/app/components/forms/forgot-password.tsx
@@ -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 (
diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx
index 1306d7e35..9b8452043 100644
--- a/apps/remix/app/components/forms/signin.tsx
+++ b/apps/remix/app/components/forms/signin.tsx
@@ -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,
// });
diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx
index 6032ec0c2..0cf100f78 100644
--- a/apps/remix/app/components/forms/signup.tsx
+++ b/apps/remix/app/components/forms/signup.tsx
@@ -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) {
diff --git a/apps/remix/app/components/forms/team-branding-preferences-form.tsx b/apps/remix/app/components/forms/team-branding-preferences-form.tsx
index 4a693618a..14f6ef66b 100644
--- a/apps/remix/app/components/forms/team-branding-preferences-form.tsx
+++ b/apps/remix/app/components/forms/team-branding-preferences-form.tsx
@@ -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]));
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
index 806a05ed4..ade23c509 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
@@ -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);
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
index 09b97e99a..dffdc1e00 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
@@ -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`),
diff --git a/apps/remix/app/components/general/document-signing/document-signing-form.tsx b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
index 3c53e3e36..20f9223e7 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-form.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
@@ -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)}
>
Cancel
@@ -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)}
>
Cancel
diff --git a/apps/remix/app/routes/_redirects+/ingest.$.tsx b/apps/remix/app/routes/_redirects+/ingest.$.tsx
new file mode 100644
index 000000000..bb5da6284
--- /dev/null
+++ b/apps/remix/app/routes/_redirects+/ingest.$.tsx
@@ -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);
+}
diff --git a/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx b/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx
index 8e86de76f..e34f98f03 100644
--- a/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx
+++ b/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx
@@ -35,5 +35,5 @@ export const loader = ({ request }: Route.LoaderArgs) => {
return null;
}
- return redirect(NEXT_PUBLIC_MARKETING_URL());
+ throw redirect(NEXT_PUBLIC_MARKETING_URL());
};
diff --git a/apps/remix/app/storage/theme-session.server.ts b/apps/remix/app/storage/theme-session.server.ts
index efbb44bb7..1b1de3dd8 100644
--- a/apps/remix/app/storage/theme-session.server.ts
+++ b/apps/remix/app/storage/theme-session.server.ts
@@ -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 } : {}),
},
});
diff --git a/apps/remix/server/load-context.ts b/apps/remix/server/load-context.ts
index 5a369971a..18fee14e0 100644
--- a/apps/remix/server/load-context.ts
+++ b/apps/remix/server/load-context.ts
@@ -13,7 +13,7 @@ declare module 'react-router' {
interface AppLoadContext extends Awaited> {}
}
-const logger = new AppLogger('[Context]');
+const logger = new AppLogger('Context');
export async function getLoadContext(args: GetLoadContextArgs) {
const initTime = Date.now();
diff --git a/packages/auth/client/index.ts b/packages/auth/client/index.ts
index db843393d..9a57d152b 100644
--- a/packages/auth/client/index.ts
+++ b/packages/auth/client/index.ts
@@ -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('http://localhost:3000/api/auth');
-
export const authClient = new AuthClient({
- baseUrl: 'http://localhost:3000/api/auth',
+ baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth`,
});
diff --git a/packages/auth/server/routes/passkey.ts b/packages/auth/server/routes/passkey.ts
index 0ec2079c9..fee7b1efc 100644
--- a/packages/auth/server/routes/passkey.ts
+++ b/packages/auth/server/routes/passkey.ts
@@ -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()
+ /**
+ * Authorize endpoint.
+ */
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
@@ -43,7 +44,7 @@ export const passkeyRoute = new Hono()
.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()
},
});
- return null;
+ throw new AppError(AppErrorCode.INVALID_REQUEST);
}
await prisma.passkey.update({
@@ -117,28 +118,29 @@ export const passkeyRoute = new Hono()
},
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,
+// });
+// },
+// );
diff --git a/packages/ee/server-only/stripe/webhook/handler.ts b/packages/ee/server-only/stripe/webhook/handler.ts
index 3039b97ad..4a1a84379 100644
--- a/packages/ee/server-only/stripe/webhook/handler.ts
+++ b/packages/ee/server-only/stripe/webhook/handler.ts
@@ -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');
diff --git a/packages/lib/client-only/hooks/use-field-item-styles.ts b/packages/lib/client-only/hooks/use-field-item-styles.ts
deleted file mode 100644
index 747ad1db2..000000000
--- a/packages/lib/client-only/hooks/use-field-item-styles.ts
+++ /dev/null
@@ -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]);
-};
diff --git a/packages/lib/client-only/providers/feature-flag.tsx b/packages/lib/client-only/providers/feature-flag.tsx
deleted file mode 100644
index aeb222766..000000000
--- a/packages/lib/client-only/providers/feature-flag.tsx
+++ /dev/null
@@ -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(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;
-}) {
- 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 (
-
- {children}
-
- );
-}
diff --git a/packages/lib/client-only/providers/feature-flag.types.ts b/packages/lib/client-only/providers/feature-flag.types.ts
deleted file mode 100644
index 1654a188c..000000000
--- a/packages/lib/client-only/providers/feature-flag.types.ts
+++ /dev/null
@@ -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;
diff --git a/packages/lib/client-only/providers/i18n-server.tsx b/packages/lib/client-only/providers/i18n-server.tsx
index da19fa369..7ed8d1978 100644
--- a/packages/lib/client-only/providers/i18n-server.tsx
+++ b/packages/lib/client-only/providers/i18n-server.tsx
@@ -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,
diff --git a/packages/lib/constants/crypto.ts b/packages/lib/constants/crypto.ts
index 9350c105c..a70f24c01 100644
--- a/packages/lib/constants/crypto.ts
+++ b/packages/lib/constants/crypto.ts
@@ -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) {
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
deleted file mode 100644
index d00966a05..000000000
--- a/packages/lib/next-auth/auth-options.ts
+++ /dev/null
@@ -1,519 +0,0 @@
-///
-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({
- 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.
-};
diff --git a/packages/lib/server-only/2fa/get-backup-code.ts b/packages/lib/server-only/2fa/get-backup-code.ts
index c225247af..3b94db05c 100644
--- a/packages/lib/server-only/2fa/get-backup-code.ts
+++ b/packages/lib/server-only/2fa/get-backup-code.ts
@@ -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');
}
diff --git a/packages/lib/server-only/2fa/validate-2fa.ts b/packages/lib/server-only/2fa/validate-2fa.ts
index 0b505fd3d..0796992ba 100644
--- a/packages/lib/server-only/2fa/validate-2fa.ts
+++ b/packages/lib/server-only/2fa/validate-2fa.ts
@@ -7,7 +7,10 @@ import { verifyBackupCode } from './verify-backup-code';
type ValidateTwoFactorAuthenticationOptions = {
totpCode?: string;
backupCode?: string;
- user: Pick;
+ 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');
diff --git a/packages/lib/server-only/2fa/verify-2fa-token.ts b/packages/lib/server-only/2fa/verify-2fa-token.ts
index 1e00ea915..0d740e084 100644
--- a/packages/lib/server-only/2fa/verify-2fa-token.ts
+++ b/packages/lib/server-only/2fa/verify-2fa-token.ts
@@ -6,7 +6,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricDecrypt } from '../../universal/crypto';
type VerifyTwoFactorAuthenticationTokenOptions = {
- user: User;
+ user: Pick;
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');
}
diff --git a/packages/lib/server-only/2fa/verify-backup-code.ts b/packages/lib/server-only/2fa/verify-backup-code.ts
index 2f4b24b71..4826da7bf 100644
--- a/packages/lib/server-only/2fa/verify-backup-code.ts
+++ b/packages/lib/server-only/2fa/verify-backup-code.ts
@@ -3,12 +3,12 @@ import type { User } from '@prisma/client';
import { getBackupCodes } from './get-backup-code';
type VerifyBackupCodeParams = {
- user: Pick;
+ user: Pick;
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');
diff --git a/packages/lib/server-only/feature-flags/all.ts b/packages/lib/server-only/feature-flags/all.ts
deleted file mode 100644
index 05163b356..000000000
--- a/packages/lib/server-only/feature-flags/all.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/lib/server-only/feature-flags/get-server-component-feature-flag.ts b/packages/lib/server-only/feature-flags/get-server-component-feature-flag.ts
deleted file mode 100644
index 9cdddd7ae..000000000
--- a/packages/lib/server-only/feature-flags/get-server-component-feature-flag.ts
+++ /dev/null
@@ -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()),
- });
-};
diff --git a/packages/lib/server-only/feature-flags/get.ts b/packages/lib/server-only/feature-flags/get.ts
deleted file mode 100644
index 46a223d14..000000000
--- a/packages/lib/server-only/feature-flags/get.ts
+++ /dev/null
@@ -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;
- personProperties?: Record;
- groupProperties?: Record>;
-} => {
- 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;
-};
diff --git a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
index 1310b0163..1d7a284ea 100644
--- a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
+++ b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
@@ -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();
}
diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
index aa85f1833..f3159f533 100644
--- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts
+++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
@@ -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);
diff --git a/packages/lib/server-only/user/forgot-password.ts b/packages/lib/server-only/user/forgot-password.ts
index a21e2a71e..3b19f1e33 100644
--- a/packages/lib/server-only/user/forgot-password.ts
+++ b/packages/lib/server-only/user/forgot-password.ts
@@ -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: {
diff --git a/packages/lib/server-only/user/update-public-profile.ts b/packages/lib/server-only/user/update-public-profile.ts
index 8c9c6eab2..0a9c85ffc 100644
--- a/packages/lib/server-only/user/update-public-profile.ts
+++ b/packages/lib/server-only/user/update-public-profile.ts
@@ -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',
});
}
diff --git a/packages/lib/universal/get-feature-flag.ts b/packages/lib/universal/get-feature-flag.ts
deleted file mode 100644
index 88c5471ca..000000000
--- a/packages/lib/universal/get-feature-flag.ts
+++ /dev/null
@@ -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 => {
- 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> => {
- 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> => {
- 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;
-}
diff --git a/packages/lib/universal/upload/server-actions.ts b/packages/lib/universal/upload/server-actions.ts
index 9900152d7..3baee5b04 100644
--- a/packages/lib/universal/upload/server-actions.ts
+++ b/packages/lib/universal/upload/server-actions.ts
@@ -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();
diff --git a/packages/lib/utils/debugger.ts b/packages/lib/utils/debugger.ts
index a0fb781d9..0ce9d6e1c 100644
--- a/packages/lib/utils/debugger.ts
+++ b/packages/lib/utils/debugger.ts
@@ -1,9 +1,7 @@
-import { env } from '@documenso/lib/utils/env';
-
export const appLog = (context: string, ...args: Parameters) => {
- 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 {
diff --git a/packages/lib/utils/env.ts b/packages/lib/utils/env.ts
index dcf7b8e90..c6c61703c 100644
--- a/packages/lib/utils/env.ts
+++ b/packages/lib/utils/env.ts
@@ -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,
diff --git a/packages/signing/transports/google-cloud-hsm.ts b/packages/signing/transports/google-cloud-hsm.ts
index ee8b29ac5..1fce63808 100644
--- a/packages/signing/transports/google-cloud-hsm.ts
+++ b/packages/signing/transports/google-cloud-hsm.ts
@@ -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) {
diff --git a/packages/signing/transports/local-cert.ts b/packages/signing/transports/local-cert.ts
index 73226bc8a..b90fc15ea 100644
--- a/packages/signing/transports/local-cert.ts
+++ b/packages/signing/transports/local-cert.ts
@@ -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) {
diff --git a/packages/trpc/server/context.ts b/packages/trpc/server/context.ts
index 4803e0e79..9a33ee48a 100644
--- a/packages/trpc/server/context.ts
+++ b/packages/trpc/server/context.ts
@@ -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;
}
) & {
diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts
index 67a7087ed..747a62e9f 100644
--- a/packages/trpc/server/trpc.ts
+++ b/packages/trpc/server/trpc.ts
@@ -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) {
diff --git a/packages/ui/primitives/multi-select-combobox.tsx b/packages/ui/primitives/multi-select-combobox.tsx
index 67b566d3b..9cf3762cb 100644
--- a/packages/ui/primitives/multi-select-combobox.tsx
+++ b/packages/ui/primitives/multi-select-combobox.tsx
@@ -22,7 +22,7 @@ type ComboBoxOption = {
};
type MultiSelectComboboxProps = {
- emptySelectionPlaceholder?: React.ReactNode | string;
+ emptySelectionPlaceholder?: React.ReactElement | string;
enableClearAllButton?: boolean;
loading?: boolean;
inputPlaceholder?: MessageDescriptor;