mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
fix: errors
This commit is contained in:
@ -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');
|
||||
|
||||
@ -426,7 +426,7 @@ export const TemplateDirectLinkDialog = ({
|
||||
await toggleTemplateDirectLink({
|
||||
templateId: template.id,
|
||||
enabled: isEnabled,
|
||||
}).catch((e) => null);
|
||||
}).catch(() => null);
|
||||
|
||||
onOpenChange(false);
|
||||
}}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
// });
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]));
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -107,7 +107,7 @@ export const DocumentSigningForm = ({
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${recipient.token}/complete`);
|
||||
await navigate(redirectUrl ? redirectUrl : `/sign/${recipient.token}/complete`);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -157,7 +157,7 @@ export const DocumentSigningForm = ({
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
@ -239,7 +239,7 @@ export const DocumentSigningForm = ({
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
41
apps/remix/app/routes/_redirects+/ingest.$.tsx
Normal file
41
apps/remix/app/routes/_redirects+/ingest.$.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* https://posthog.com/docs/advanced/proxy/remix
|
||||
*/
|
||||
import type { Route } from './+types/ingest.$';
|
||||
|
||||
const API_HOST = 'eu.i.posthog.com';
|
||||
const ASSET_HOST = 'eu-assets.i.posthog.com';
|
||||
|
||||
const posthogProxy = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
const hostname = url.pathname.startsWith('/ingest/static/') ? ASSET_HOST : API_HOST;
|
||||
|
||||
const newUrl = new URL(url);
|
||||
newUrl.protocol = 'https';
|
||||
newUrl.hostname = hostname;
|
||||
newUrl.port = '443';
|
||||
newUrl.pathname = newUrl.pathname.replace(/^\/ingest/, '');
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set('host', hostname);
|
||||
|
||||
const response = await fetch(newUrl, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: request.body,
|
||||
});
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
});
|
||||
};
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
return posthogProxy(request);
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
return posthogProxy(request);
|
||||
}
|
||||
@ -35,5 +35,5 @@ export const loader = ({ request }: Route.LoaderArgs) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redirect(NEXT_PUBLIC_MARKETING_URL());
|
||||
throw redirect(NEXT_PUBLIC_MARKETING_URL());
|
||||
};
|
||||
|
||||
@ -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 } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ declare module 'react-router' {
|
||||
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
|
||||
}
|
||||
|
||||
const logger = new AppLogger('[Context]');
|
||||
const logger = new AppLogger('Context');
|
||||
|
||||
export async function getLoadContext(args: GetLoadContextArgs) {
|
||||
const initTime = Date.now();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ClientResponse } from 'hono/client';
|
||||
import { hc } from 'hono/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import type { AuthAppType } from '../server';
|
||||
@ -107,10 +108,6 @@ export class AuthClient {
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: Env
|
||||
// Todo: Remove in favor of AuthClient
|
||||
// export const authClient = hc<AuthAppType>('http://localhost:3000/api/auth');
|
||||
|
||||
export const authClient = new AuthClient({
|
||||
baseUrl: 'http://localhost:3000/api/auth',
|
||||
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth`,
|
||||
});
|
||||
|
||||
@ -2,7 +2,6 @@ import { zValidator } from '@hono/zod-validator';
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
@ -11,11 +10,13 @@ import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { onAuthorize } from '../lib/utils/authorizer';
|
||||
import { getRequiredSession } from '../lib/utils/get-session';
|
||||
import type { HonoAuthContext } from '../types/context';
|
||||
import { ZPasskeyAuthorizeSchema } from '../types/passkey';
|
||||
|
||||
export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
/**
|
||||
* Authorize endpoint.
|
||||
*/
|
||||
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
@ -43,7 +44,7 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
.catch(() => null);
|
||||
|
||||
if (!challengeToken) {
|
||||
return null;
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
if (challengeToken.expiresAt < new Date()) {
|
||||
@ -96,7 +97,7 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
await prisma.passkey.update({
|
||||
@ -117,28 +118,29 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
},
|
||||
200,
|
||||
);
|
||||
})
|
||||
|
||||
.post('/register', async (c) => {
|
||||
const { user } = await getRequiredSession(c);
|
||||
|
||||
//
|
||||
})
|
||||
|
||||
.post(
|
||||
'/pre-authenticate',
|
||||
zValidator(
|
||||
'json',
|
||||
z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
//
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Todo
|
||||
// .post('/register', async (c) => {
|
||||
// const { user } = await getRequiredSession(c);
|
||||
|
||||
// //
|
||||
// })
|
||||
|
||||
// .post(
|
||||
// '/pre-authenticate',
|
||||
// zValidator(
|
||||
// 'json',
|
||||
// z.object({
|
||||
// code: z.string(),
|
||||
// }),
|
||||
// ),
|
||||
// async (c) => {
|
||||
// //
|
||||
|
||||
// return c.json({
|
||||
// success: true,
|
||||
// recoveryCodes: result.recoveryCodes,
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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]);
|
||||
};
|
||||
@ -1,93 +0,0 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
FEATURE_FLAG_POLL_INTERVAL,
|
||||
LOCAL_FEATURE_FLAGS,
|
||||
isFeatureFlagEnabled,
|
||||
} from '@documenso/lib/constants/feature-flags';
|
||||
import { getAllFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
|
||||
import type { TFeatureFlagValue } from './feature-flag.types';
|
||||
|
||||
export type FeatureFlagContextValue = {
|
||||
getFlag: (_key: string) => TFeatureFlagValue;
|
||||
};
|
||||
|
||||
export const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
|
||||
|
||||
export const useFeatureFlags = () => {
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useFeatureFlags must be used within a FeatureFlagProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export function FeatureFlagProvider({
|
||||
children,
|
||||
initialFlags,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialFlags: Record<string, TFeatureFlagValue>;
|
||||
}) {
|
||||
const [flags, setFlags] = useState(initialFlags);
|
||||
|
||||
const getFlag = useCallback(
|
||||
(flag: string) => {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return LOCAL_FEATURE_FLAGS[flag] ?? true;
|
||||
}
|
||||
|
||||
return flags[flag] ?? false;
|
||||
},
|
||||
[flags],
|
||||
);
|
||||
|
||||
/**
|
||||
* Refresh the flags every `FEATURE_FLAG_POLL_INTERVAL` amount of time if the window is focused.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (document.hasFocus()) {
|
||||
void getAllFlags().then((newFlags) => setFlags(newFlags));
|
||||
}
|
||||
}, FEATURE_FLAG_POLL_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Refresh the flags when the window is focused.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onFocus = () => void getAllFlags().then((newFlags) => setFlags(newFlags));
|
||||
|
||||
window.addEventListener('focus', onFocus);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FeatureFlagContext.Provider
|
||||
value={{
|
||||
getFlag,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FeatureFlagContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZFeatureFlagValueSchema = z.union([
|
||||
z.boolean(),
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.undefined(),
|
||||
]);
|
||||
|
||||
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,519 +0,0 @@
|
||||
/// <reference types="../types/next-auth.d.ts" />
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||
import { compare } from '@node-rs/bcrypt';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { IdentityProvider, UserSecurityAuditLogType } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { AuthOptions, Session, User } from 'next-auth';
|
||||
import type { JWT } from 'next-auth/jwt';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import type { GoogleProfile } from 'next-auth/providers/google';
|
||||
import GoogleProvider from 'next-auth/providers/google';
|
||||
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
import { jobsClient } from '../jobs/client';
|
||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
||||
import { decryptSecondaryData } from '../server-only/crypto/decrypt';
|
||||
import { getMostRecentVerificationTokenByUserId } from '../server-only/user/get-most-recent-verification-token-by-user-id';
|
||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||
import type { TAuthenticationResponseJSONSchema } from '../types/webauthn';
|
||||
import { ZAuthenticationResponseJSONSchema } from '../types/webauthn';
|
||||
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
|
||||
import { getAuthenticatorOptions } from '../utils/authenticator';
|
||||
import { ErrorCode } from './error-codes';
|
||||
|
||||
// Delete unrecognized fields from authorization response to comply with
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
||||
const prismaAdapter = PrismaAdapter(prisma);
|
||||
|
||||
const unsafe_linkAccount = prismaAdapter.linkAccount!;
|
||||
const unsafe_accountModel = Prisma.dmmf.datamodel.models.find(({ name }) => name === 'Account');
|
||||
|
||||
if (!unsafe_accountModel) {
|
||||
throw new Error('Account model not found');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
prismaAdapter.linkAccount = (data) => {
|
||||
const availableFields = unsafe_accountModel.fields.map((field) => field.name);
|
||||
|
||||
const newData = Object.keys(data).reduce(
|
||||
(acc, key) => {
|
||||
if (availableFields.includes(key)) {
|
||||
acc[key] = data[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
{} as typeof data,
|
||||
);
|
||||
|
||||
return unsafe_linkAccount(newData);
|
||||
};
|
||||
|
||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
adapter: prismaAdapter,
|
||||
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
},
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
totpCode: {
|
||||
label: 'Two-factor Code',
|
||||
type: 'input',
|
||||
placeholder: 'Code from authenticator app',
|
||||
},
|
||||
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
|
||||
},
|
||||
authorize: async (credentials, req) => {
|
||||
if (!credentials) {
|
||||
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
|
||||
}
|
||||
|
||||
const { email, password, backupCode, totpCode } = credentials;
|
||||
|
||||
const user = await getUserByEmail({ email }).catch(() => {
|
||||
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
|
||||
});
|
||||
|
||||
if (!user.password) {
|
||||
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
|
||||
}
|
||||
|
||||
const isPasswordsSame = await compare(password, user.password);
|
||||
const requestMetadata = extractNextAuthRequestMetadata(req);
|
||||
|
||||
if (!isPasswordsSame) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
|
||||
},
|
||||
});
|
||||
|
||||
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
|
||||
}
|
||||
|
||||
const is2faEnabled = isTwoFactorAuthenticationEnabled({ user });
|
||||
|
||||
if (is2faEnabled) {
|
||||
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
|
||||
|
||||
if (!isValid) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN_2FA_FAIL,
|
||||
},
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
totpCode
|
||||
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
|
||||
: ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.emailVerified) {
|
||||
const mostRecentToken = await getMostRecentVerificationTokenByUserId({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (
|
||||
!mostRecentToken ||
|
||||
mostRecentToken.expires.valueOf() <= Date.now() ||
|
||||
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
|
||||
) {
|
||||
await jobsClient.triggerJob({
|
||||
name: 'send.signup.confirmation.email',
|
||||
payload: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
throw new Error(ErrorCode.ACCOUNT_DISABLED);
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(user.id),
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
emailVerified: user.emailVerified?.toISOString() ?? null,
|
||||
} satisfies User;
|
||||
},
|
||||
}),
|
||||
GoogleProvider<GoogleProfile>({
|
||||
clientId: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID ?? '',
|
||||
clientSecret: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET ?? '',
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
|
||||
profile(profile) {
|
||||
return {
|
||||
id: Number(profile.sub),
|
||||
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
|
||||
email: profile.email,
|
||||
emailVerified: profile.email_verified ? new Date().toISOString() : null,
|
||||
};
|
||||
},
|
||||
}),
|
||||
{
|
||||
id: 'oidc',
|
||||
name: 'OIDC',
|
||||
type: 'oauth',
|
||||
|
||||
wellKnown: process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN,
|
||||
clientId: process.env.NEXT_PRIVATE_OIDC_CLIENT_ID,
|
||||
clientSecret: process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
|
||||
|
||||
authorization: { params: { scope: 'openid email profile' } },
|
||||
checks: ['pkce', 'state'],
|
||||
|
||||
idToken: true,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
email: profile.email || profile.preferred_username,
|
||||
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
|
||||
emailVerified:
|
||||
process.env.NEXT_PRIVATE_OIDC_SKIP_VERIFY === 'true' || profile.email_verified
|
||||
? new Date().toISOString()
|
||||
: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
CredentialsProvider({
|
||||
id: 'webauthn',
|
||||
name: 'Keypass',
|
||||
credentials: {
|
||||
csrfToken: { label: 'csrfToken', type: 'csrfToken' },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const csrfToken = credentials?.csrfToken;
|
||||
|
||||
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
|
||||
|
||||
try {
|
||||
const parsedBodyCredential = JSON.parse(req.body?.credential);
|
||||
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
|
||||
} catch {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const challengeToken = await prisma.anonymousVerificationToken
|
||||
.delete({
|
||||
where: {
|
||||
id: csrfToken,
|
||||
},
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!challengeToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (challengeToken.expiresAt < new Date()) {
|
||||
throw new AppError(AppErrorCode.EXPIRED_CODE);
|
||||
}
|
||||
|
||||
const passkey = await prisma.passkey.findFirst({
|
||||
where: {
|
||||
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!passkey) {
|
||||
throw new AppError(AppErrorCode.NOT_SETUP);
|
||||
}
|
||||
|
||||
const user = passkey.user;
|
||||
|
||||
const { rpId, origin } = getAuthenticatorOptions();
|
||||
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
response: requestBodyCrediential,
|
||||
expectedChallenge: challengeToken.token,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: rpId,
|
||||
authenticator: {
|
||||
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
|
||||
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
|
||||
counter: Number(passkey.counter),
|
||||
},
|
||||
}).catch(() => null);
|
||||
|
||||
const requestMetadata = extractNextAuthRequestMetadata(req);
|
||||
|
||||
if (!verification?.verified) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
await prisma.passkey.update({
|
||||
where: {
|
||||
id: passkey.id,
|
||||
},
|
||||
data: {
|
||||
lastUsedAt: new Date(),
|
||||
counter: verification.authenticationInfo.newCounter,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: Number(user.id),
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
emailVerified: user.emailVerified?.toISOString() ?? null,
|
||||
} satisfies User;
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: 'manual',
|
||||
name: 'Manual',
|
||||
credentials: {
|
||||
credential: { label: 'Credential', type: 'credential' },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const credential = credentials?.credential;
|
||||
|
||||
if (typeof credential !== 'string' || credential.length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const decryptedCredential = decryptSecondaryData(credential);
|
||||
|
||||
if (!decryptedCredential) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const parsedCredential = JSON.parse(decryptedCredential);
|
||||
|
||||
if (typeof parsedCredential !== 'object' || parsedCredential === null) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const { userId, email } = parsedCredential;
|
||||
|
||||
if (typeof userId !== 'number' || typeof email !== 'string') {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(user.id),
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
emailVerified: user.emailVerified?.toISOString() ?? null,
|
||||
} satisfies User;
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, trigger, account }) {
|
||||
const merged = {
|
||||
...token,
|
||||
...user,
|
||||
emailVerified: user?.emailVerified ? new Date(user.emailVerified).toISOString() : null,
|
||||
} satisfies JWT;
|
||||
|
||||
if (!merged.email || typeof merged.emailVerified !== 'string') {
|
||||
const userId = Number(merged.id ?? token.sub);
|
||||
|
||||
const retrieved = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!retrieved) {
|
||||
return token;
|
||||
}
|
||||
|
||||
merged.id = retrieved.id;
|
||||
merged.name = retrieved.name;
|
||||
merged.email = retrieved.email;
|
||||
merged.emailVerified = retrieved.emailVerified?.toISOString() ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
merged.id &&
|
||||
(!merged.lastSignedIn ||
|
||||
DateTime.fromISO(merged.lastSignedIn).plus({ hours: 1 }) <= DateTime.now())
|
||||
) {
|
||||
merged.lastSignedIn = new Date().toISOString();
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: Number(merged.id),
|
||||
},
|
||||
data: {
|
||||
lastSignedIn: merged.lastSignedIn,
|
||||
},
|
||||
});
|
||||
|
||||
merged.emailVerified = user.emailVerified?.toISOString() ?? null;
|
||||
}
|
||||
|
||||
if ((trigger === 'signIn' || trigger === 'signUp') && account?.provider === 'google') {
|
||||
merged.emailVerified = user?.emailVerified
|
||||
? new Date(user.emailVerified).toISOString()
|
||||
: new Date().toISOString();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: Number(merged.id),
|
||||
},
|
||||
data: {
|
||||
emailVerified: merged.emailVerified,
|
||||
identityProvider: IdentityProvider.GOOGLE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: merged.id,
|
||||
name: merged.name,
|
||||
email: merged.email,
|
||||
lastSignedIn: merged.lastSignedIn,
|
||||
emailVerified: merged.emailVerified,
|
||||
} satisfies JWT;
|
||||
},
|
||||
|
||||
session({ token, session }) {
|
||||
if (token && token.email) {
|
||||
return {
|
||||
...session,
|
||||
user: {
|
||||
id: Number(token.id),
|
||||
name: token.name,
|
||||
email: token.email,
|
||||
emailVerified: token.emailVerified ?? null,
|
||||
},
|
||||
} satisfies Session;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
|
||||
async signIn({ user }) {
|
||||
// This statement appears above so we can stil allow `oidc` connections
|
||||
// while other signups are disabled.
|
||||
if (env('NEXT_PRIVATE_OIDC_ALLOW_SIGNUP') === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We do this to stop OAuth providers from creating an account
|
||||
// when signups are disabled
|
||||
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
|
||||
const userData = await getUserByEmail({ email: user.email! });
|
||||
|
||||
return !!userData;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: formatSecureCookieName('next-auth.session-token'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
callbackUrl: {
|
||||
name: formatSecureCookieName('next-auth.callback-url'),
|
||||
options: {
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
csrfToken: {
|
||||
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
|
||||
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
|
||||
name: formatSecureCookieName('next-auth.csrf-token'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
pkceCodeVerifier: {
|
||||
name: formatSecureCookieName('next-auth.pkce.code_verifier'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
name: formatSecureCookieName('next-auth.state'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Note: `events` are handled in `apps/web/src/pages/api/auth/[...nextauth].ts` to allow access to the request.
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -7,7 +7,10 @@ import { verifyBackupCode } from './verify-backup-code';
|
||||
type ValidateTwoFactorAuthenticationOptions = {
|
||||
totpCode?: string;
|
||||
backupCode?: string;
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
|
||||
user: Pick<
|
||||
User,
|
||||
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
|
||||
>;
|
||||
};
|
||||
|
||||
export const validateTwoFactorAuthentication = async ({
|
||||
@ -28,7 +31,7 @@ export const validateTwoFactorAuthentication = async ({
|
||||
}
|
||||
|
||||
if (backupCode) {
|
||||
return await verifyBackupCode({ user, backupCode });
|
||||
return verifyBackupCode({ user, backupCode });
|
||||
}
|
||||
|
||||
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
|
||||
|
||||
@ -6,7 +6,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricDecrypt } from '../../universal/crypto';
|
||||
|
||||
type VerifyTwoFactorAuthenticationTokenOptions = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'twoFactorSecret'>;
|
||||
totpCode: string;
|
||||
// The number of windows to look back
|
||||
window?: number;
|
||||
@ -22,6 +22,10 @@ export const verifyTwoFactorAuthenticationToken = async ({
|
||||
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||
|
||||
if (!key) {
|
||||
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
throw new Error('user missing 2fa secret');
|
||||
}
|
||||
|
||||
@ -3,12 +3,12 @@ import type { User } from '@prisma/client';
|
||||
import { getBackupCodes } from './get-backup-code';
|
||||
|
||||
type VerifyBackupCodeParams = {
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
||||
user: Pick<User, 'id' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
||||
backupCode: string;
|
||||
};
|
||||
|
||||
export const verifyBackupCode = async ({ user, backupCode }: VerifyBackupCodeParams) => {
|
||||
const userBackupCodes = await getBackupCodes({ user });
|
||||
export const verifyBackupCode = ({ user, backupCode }: VerifyBackupCodeParams) => {
|
||||
const userBackupCodes = getBackupCodes({ user });
|
||||
|
||||
if (!userBackupCodes) {
|
||||
throw new Error('User has no backup codes');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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()),
|
||||
});
|
||||
};
|
||||
@ -1,136 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { JWT } from 'next-auth/jwt';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
|
||||
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
|
||||
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
/**
|
||||
* Evaluate a single feature flag based on the current user if possible.
|
||||
*
|
||||
* @param req The request with a query parameter `flag`. Example request URL: /api/feature-flag/get?flag=flag-name
|
||||
* @returns A Response with the feature flag value.
|
||||
*/
|
||||
export default async function handleFeatureFlagGet(req: Request) {
|
||||
const { searchParams } = new URL(req.url ?? '');
|
||||
const flag = searchParams.get('flag');
|
||||
|
||||
const requestHeaders = Object.fromEntries(req.headers.entries());
|
||||
|
||||
const nextReq = new NextRequest(req, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
const token = await getToken({ req: nextReq });
|
||||
|
||||
if (!flag) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Missing flag query parameter.',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const postHog = PostHogServerClient();
|
||||
|
||||
// Return the local feature flags if PostHog is not enabled, true by default.
|
||||
// The front end should not call this API if PostHog is disabled to reduce network requests.
|
||||
if (!postHog) {
|
||||
return NextResponse.json(LOCAL_FEATURE_FLAGS[flag] ?? true);
|
||||
}
|
||||
|
||||
const distinctId = extractDistinctUserId(token, nextReq);
|
||||
|
||||
const featureFlag = await postHog.getFeatureFlag(flag, distinctId, mapJwtToFlagProperties(token));
|
||||
|
||||
const res = NextResponse.json(featureFlag);
|
||||
|
||||
res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
|
||||
|
||||
const origin = req.headers.get('Origin');
|
||||
|
||||
if (origin) {
|
||||
if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
|
||||
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
|
||||
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a JWT to properties which are consumed by PostHog to evaluate feature flags.
|
||||
*
|
||||
* @param jwt The JWT of the current user.
|
||||
* @returns A map of properties which are consumed by PostHog.
|
||||
*/
|
||||
export const mapJwtToFlagProperties = (
|
||||
jwt?: JWT | null,
|
||||
): {
|
||||
groups?: Record<string, string>;
|
||||
personProperties?: Record<string, string>;
|
||||
groupProperties?: Record<string, Record<string, string>>;
|
||||
} => {
|
||||
return {
|
||||
personProperties: {
|
||||
email: jwt?.email ?? '',
|
||||
},
|
||||
groupProperties: {
|
||||
// Add properties to group users into different groups, such as billing plan.
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract a distinct ID from a JWT and request.
|
||||
*
|
||||
* Will fallback to a random ID if no ID could be extracted from either the JWT or request.
|
||||
*
|
||||
* @param jwt The JWT of the current user.
|
||||
* @param request Request potentially containing a PostHog `distinct_id` cookie.
|
||||
* @returns A distinct user ID.
|
||||
*/
|
||||
export const extractDistinctUserId = (jwt: JWT | null, request: NextRequest): string => {
|
||||
const config = extractPostHogConfig();
|
||||
|
||||
const email = jwt?.email;
|
||||
const userId = jwt?.id?.toString();
|
||||
|
||||
let fallbackDistinctId = nanoid();
|
||||
|
||||
if (config) {
|
||||
try {
|
||||
const postHogCookie = JSON.parse(
|
||||
request.cookies.get(`ph_${config.key}_posthog`)?.value ?? '',
|
||||
);
|
||||
|
||||
const postHogDistinctId = postHogCookie['distinct_id'];
|
||||
|
||||
if (typeof postHogDistinctId === 'string') {
|
||||
fallbackDistinctId = postHogDistinctId;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return email ?? userId ?? fallbackDistinctId;
|
||||
};
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { TFeatureFlagValue } from '@documenso/lib/client-only/providers/feature-flag.types';
|
||||
import { ZFeatureFlagValueSchema } from '@documenso/lib/client-only/providers/feature-flag.types';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
||||
|
||||
/**
|
||||
* Evaluate whether a flag is enabled for the current user.
|
||||
*
|
||||
* @param flag The flag to evaluate.
|
||||
* @param options See `GetFlagOptions`.
|
||||
* @returns Whether the flag is enabled, or the variant value of the flag.
|
||||
*/
|
||||
export const getFlag = async (
|
||||
flag: string,
|
||||
options?: GetFlagOptions,
|
||||
): Promise<TFeatureFlagValue> => {
|
||||
const requestHeaders = options?.requestHeaders ?? {};
|
||||
delete requestHeaders['content-length'];
|
||||
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return LOCAL_FEATURE_FLAGS[flag] ?? true;
|
||||
}
|
||||
|
||||
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/get`);
|
||||
url.searchParams.set('flag', flag);
|
||||
|
||||
return await fetch(url, {
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
})
|
||||
.then(async (res) => res.json())
|
||||
.then((res) => ZFeatureFlagValueSchema.parse(res))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return LOCAL_FEATURE_FLAGS[flag] ?? false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all feature flags for the current user if possible.
|
||||
*
|
||||
* @param options See `GetFlagOptions`.
|
||||
* @returns A record of flags and their values for the user derived from the headers.
|
||||
*/
|
||||
export const getAllFlags = async (
|
||||
options?: GetFlagOptions,
|
||||
): Promise<Record<string, TFeatureFlagValue>> => {
|
||||
const requestHeaders = options?.requestHeaders ?? {};
|
||||
delete requestHeaders['content-length'];
|
||||
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return LOCAL_FEATURE_FLAGS;
|
||||
}
|
||||
|
||||
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
})
|
||||
.then(async (res) => res.json())
|
||||
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return LOCAL_FEATURE_FLAGS;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all feature flags for anonymous users.
|
||||
*
|
||||
* @returns A record of flags and their values.
|
||||
*/
|
||||
export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFlagValue>> => {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return LOCAL_FEATURE_FLAGS;
|
||||
}
|
||||
|
||||
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
|
||||
|
||||
return fetch(url, {
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
})
|
||||
.then(async (res) => res.json())
|
||||
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return LOCAL_FEATURE_FLAGS;
|
||||
});
|
||||
};
|
||||
|
||||
interface GetFlagOptions {
|
||||
/**
|
||||
* The headers to attach to the request to evaluate flags.
|
||||
*
|
||||
* The authenticated user will be derived from the headers if possible.
|
||||
*/
|
||||
requestHeaders: Record<string, string>;
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
export const appLog = (context: string, ...args: Parameters<typeof console.log>) => {
|
||||
if (env('NEXT_DEBUG') === 'true') {
|
||||
// if (env('NEXT_DEBUG') === 'true') {
|
||||
console.log(`[${context}]: ${args[0]}`, ...args.slice(1));
|
||||
}
|
||||
// }
|
||||
};
|
||||
|
||||
export class AppLogger {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -17,18 +17,18 @@ export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOpti
|
||||
throw new Error('No certificate path provided for Google Cloud HSM signing');
|
||||
}
|
||||
|
||||
// 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'),
|
||||
const googleApplicationCredentials = env('GOOGLE_APPLICATION_CREDENTIALS');
|
||||
const googleApplicationCredentialsContents = env(
|
||||
'NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS',
|
||||
);
|
||||
|
||||
fs.writeFileSync(env('GOOGLE_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 (googleApplicationCredentials && googleApplicationCredentialsContents) {
|
||||
if (!fs.existsSync(googleApplicationCredentials)) {
|
||||
const contents = new Uint8Array(Buffer.from(googleApplicationCredentialsContents, 'base64'));
|
||||
|
||||
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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
) & {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -22,7 +22,7 @@ type ComboBoxOption<T = OptionValue> = {
|
||||
};
|
||||
|
||||
type MultiSelectComboboxProps<T = OptionValue> = {
|
||||
emptySelectionPlaceholder?: React.ReactNode | string;
|
||||
emptySelectionPlaceholder?: React.ReactElement | string;
|
||||
enableClearAllButton?: boolean;
|
||||
loading?: boolean;
|
||||
inputPlaceholder?: MessageDescriptor;
|
||||
|
||||
Reference in New Issue
Block a user