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;