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);
|
setIsUploadingFile(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Todo
|
||||||
// const { type, data } = await putPdfFile(file);
|
// const { type, data } = await putPdfFile(file);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@ -56,7 +57,7 @@ export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogP
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => await res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Upload failed:', e);
|
console.error('Upload failed:', e);
|
||||||
throw new AppError('UPLOAD_FAILED');
|
throw new AppError('UPLOAD_FAILED');
|
||||||
|
|||||||
@ -426,7 +426,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
await toggleTemplateDirectLink({
|
await toggleTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
}).catch((e) => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -159,7 +159,7 @@ export function TemplateUseDialog({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => await res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Upload failed:', e);
|
console.error('Upload failed:', e);
|
||||||
throw new AppError('UPLOAD_FAILED');
|
throw new AppError('UPLOAD_FAILED');
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -44,10 +44,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
||||||
await forgotPassword({ email }).catch(() => null);
|
await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
|
||||||
|
|
||||||
|
await navigate('/check-email');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Reset email sent`),
|
title: _(msg`Reset email sent`),
|
||||||
@ -58,8 +58,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
navigate('/check-email');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -270,6 +270,8 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onSignInWithOIDCClick = async () => {
|
const onSignInWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
// await signIn('oidc', {
|
// await signIn('oidc', {
|
||||||
// callbackUrl,
|
// callbackUrl,
|
||||||
// });
|
// });
|
||||||
|
|||||||
@ -185,6 +185,7 @@ export const SignUpForm = ({
|
|||||||
|
|
||||||
const onSignUpWithOIDCClick = async () => {
|
const onSignUpWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
|
|||||||
void fetch(`/api/file?key=${file.key}`, {
|
void fetch(`/api/file?key=${file.key}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => await res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const objectUrl = URL.createObjectURL(new Blob([data.binaryData]));
|
const objectUrl = URL.createObjectURL(new Blob([data.binaryData]));
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export const DocumentSigningAuthAccount = ({
|
|||||||
// // Todo: Redirect to signin like below
|
// // Todo: Redirect to signin like below
|
||||||
// }
|
// }
|
||||||
|
|
||||||
navigate(`/signin#email=${email}`);
|
await navigate(`/signin#email=${email}`);
|
||||||
} catch {
|
} catch {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const DocumentSigningAuthPageView = ({
|
|||||||
// Todo: Redirect false
|
// Todo: Redirect false
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
|
|
||||||
navigate(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
await navigate(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@ -107,7 +107,7 @@ export const DocumentSigningForm = ({
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${recipient.token}/complete`);
|
await navigate(redirectUrl ? redirectUrl : `/sign/${recipient.token}/complete`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -157,7 +157,7 @@ export const DocumentSigningForm = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => navigate(-1)}
|
onClick={async () => navigate(-1)}
|
||||||
>
|
>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -239,7 +239,7 @@ export const DocumentSigningForm = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => navigate(-1)}
|
onClick={async () => navigate(-1)}
|
||||||
>
|
>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</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 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 { createThemeSessionResolver } from 'remix-themes';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
const themeSessionStorage = createCookieSessionStorage({
|
const themeSessionStorage = createCookieSessionStorage({
|
||||||
cookie: {
|
cookie: {
|
||||||
@ -12,7 +13,7 @@ const themeSessionStorage = createCookieSessionStorage({
|
|||||||
secrets: ['insecure-secret'], // Todo: Don't need secret
|
secrets: ['insecure-secret'], // Todo: Don't need secret
|
||||||
// Todo: Check this works on production.
|
// Todo: Check this works on production.
|
||||||
// Set domain and secure only if in 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>> {}
|
interface AppLoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = new AppLogger('[Context]');
|
const logger = new AppLogger('Context');
|
||||||
|
|
||||||
export async function getLoadContext(args: GetLoadContextArgs) {
|
export async function getLoadContext(args: GetLoadContextArgs) {
|
||||||
const initTime = Date.now();
|
const initTime = Date.now();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { ClientResponse } from 'hono/client';
|
import type { ClientResponse } from 'hono/client';
|
||||||
import { hc } 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 { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
|
||||||
import type { AuthAppType } from '../server';
|
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({
|
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 { UserSecurityAuditLogType } from '@prisma/client';
|
||||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
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 { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { onAuthorize } from '../lib/utils/authorizer';
|
import { onAuthorize } from '../lib/utils/authorizer';
|
||||||
import { getRequiredSession } from '../lib/utils/get-session';
|
|
||||||
import type { HonoAuthContext } from '../types/context';
|
import type { HonoAuthContext } from '../types/context';
|
||||||
import { ZPasskeyAuthorizeSchema } from '../types/passkey';
|
import { ZPasskeyAuthorizeSchema } from '../types/passkey';
|
||||||
|
|
||||||
export const passkeyRoute = new Hono<HonoAuthContext>()
|
export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||||
|
/**
|
||||||
|
* Authorize endpoint.
|
||||||
|
*/
|
||||||
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
|
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
|||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
if (!challengeToken) {
|
if (!challengeToken) {
|
||||||
return null;
|
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (challengeToken.expiresAt < new Date()) {
|
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({
|
await prisma.passkey.update({
|
||||||
@ -117,28 +118,29 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
|||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
.post('/register', async (c) => {
|
// Todo
|
||||||
const { user } = await getRequiredSession(c);
|
// .post('/register', async (c) => {
|
||||||
|
// const { user } = await getRequiredSession(c);
|
||||||
|
|
||||||
//
|
// //
|
||||||
})
|
// })
|
||||||
|
|
||||||
.post(
|
// .post(
|
||||||
'/pre-authenticate',
|
// '/pre-authenticate',
|
||||||
zValidator(
|
// zValidator(
|
||||||
'json',
|
// 'json',
|
||||||
z.object({
|
// z.object({
|
||||||
code: z.string(),
|
// code: z.string(),
|
||||||
}),
|
// }),
|
||||||
),
|
// ),
|
||||||
async (c) => {
|
// async (c) => {
|
||||||
//
|
// //
|
||||||
|
|
||||||
return c.json({
|
// return c.json({
|
||||||
success: true,
|
// success: true,
|
||||||
recoveryCodes: result.recoveryCodes,
|
// recoveryCodes: result.recoveryCodes,
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { 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) => {
|
export const stripeWebhookHandler = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
// Todo
|
const isBillingEnabled = IS_BILLING_ENABLED();
|
||||||
// const isBillingEnabled = await getFlag('app_billing');
|
|
||||||
const isBillingEnabled = true;
|
|
||||||
|
|
||||||
const webhookSecret = env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET');
|
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';
|
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 } = await import(`../../translations/${lang}/web.${extension}`);
|
||||||
const messages = {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[lang]: messages,
|
[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 (typeof window === 'undefined') {
|
||||||
if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
|
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) => {
|
export const getBackupCodes = ({ user }: GetBackupCodesOptions) => {
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.twoFactorEnabled) {
|
if (!user.twoFactorEnabled) {
|
||||||
throw new Error('User has not enabled 2FA');
|
throw new Error('User has not enabled 2FA');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,10 @@ import { verifyBackupCode } from './verify-backup-code';
|
|||||||
type ValidateTwoFactorAuthenticationOptions = {
|
type ValidateTwoFactorAuthenticationOptions = {
|
||||||
totpCode?: string;
|
totpCode?: string;
|
||||||
backupCode?: string;
|
backupCode?: string;
|
||||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
|
user: Pick<
|
||||||
|
User,
|
||||||
|
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateTwoFactorAuthentication = async ({
|
export const validateTwoFactorAuthentication = async ({
|
||||||
@ -28,7 +31,7 @@ export const validateTwoFactorAuthentication = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (backupCode) {
|
if (backupCode) {
|
||||||
return await verifyBackupCode({ user, backupCode });
|
return verifyBackupCode({ user, backupCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
|
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
|||||||
import { symmetricDecrypt } from '../../universal/crypto';
|
import { symmetricDecrypt } from '../../universal/crypto';
|
||||||
|
|
||||||
type VerifyTwoFactorAuthenticationTokenOptions = {
|
type VerifyTwoFactorAuthenticationTokenOptions = {
|
||||||
user: User;
|
user: Pick<User, 'id' | 'twoFactorSecret'>;
|
||||||
totpCode: string;
|
totpCode: string;
|
||||||
// The number of windows to look back
|
// The number of windows to look back
|
||||||
window?: number;
|
window?: number;
|
||||||
@ -22,6 +22,10 @@ export const verifyTwoFactorAuthenticationToken = async ({
|
|||||||
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.twoFactorSecret) {
|
if (!user.twoFactorSecret) {
|
||||||
throw new Error('user missing 2fa secret');
|
throw new Error('user missing 2fa secret');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import type { User } from '@prisma/client';
|
|||||||
import { getBackupCodes } from './get-backup-code';
|
import { getBackupCodes } from './get-backup-code';
|
||||||
|
|
||||||
type VerifyBackupCodeParams = {
|
type VerifyBackupCodeParams = {
|
||||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
user: Pick<User, 'id' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
||||||
backupCode: string;
|
backupCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyBackupCode = async ({ user, backupCode }: VerifyBackupCodeParams) => {
|
export const verifyBackupCode = ({ user, backupCode }: VerifyBackupCodeParams) => {
|
||||||
const userBackupCodes = await getBackupCodes({ user });
|
const userBackupCodes = getBackupCodes({ user });
|
||||||
|
|
||||||
if (!userBackupCodes) {
|
if (!userBackupCodes) {
|
||||||
throw new Error('User has no backup codes');
|
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;
|
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.
|
// !: 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.
|
// !: 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 {
|
} else {
|
||||||
browser = await chromium.launch();
|
browser = await chromium.launch();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,9 +28,16 @@ import {
|
|||||||
import { env } from '../../utils/env';
|
import { env } from '../../utils/env';
|
||||||
|
|
||||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
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);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
|
|
||||||
|
|
||||||
import { ONE_DAY } from '../../constants/time';
|
import { ONE_DAY } from '../../constants/time';
|
||||||
import { sendForgotPassword } from '../auth/send-forgot-password';
|
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({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: {
|
email: {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileO
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isUrlTakenByAnotherUser || isUrlTakenByAnotherTeam) {
|
if (isUrlTakenByAnotherUser || isUrlTakenByAnotherTeam) {
|
||||||
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
|
throw new AppError('PROFILE_URL_TAKEN', {
|
||||||
message: 'The profile username is already 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,
|
S3Client,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import slugify from '@sindresorhus/slugify';
|
import slugify from '@sindresorhus/slugify';
|
||||||
import { type JWT } from 'next-auth/jwt';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { env } from '@documenso/lib/utils/env';
|
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 { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||||
|
|
||||||
const token: JWT | null = null;
|
const token: { id: string } | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = NEXT_PUBLIC_WEBAPP_URL();
|
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>) => {
|
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));
|
console.log(`[${context}]: ${args[0]}`, ...args.slice(1));
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AppLogger {
|
export class AppLogger {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
type EnvironmentVariable = keyof NodeJS.ProcessEnv;
|
type EnvironmentVariable = keyof NodeJS.ProcessEnv;
|
||||||
|
|
||||||
export const env = (variable: EnvironmentVariable | (string & {})): string | undefined => {
|
export const env = (variable: EnvironmentVariable | (string & object)): string | undefined => {
|
||||||
// console.log({
|
// console.log({
|
||||||
// ['typeof window']: typeof window,
|
// ['typeof window']: typeof window,
|
||||||
// ['process.env']: process.env,
|
// ['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');
|
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
|
// 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
|
// application credentials as an environment variable and write it to a file if it doesn't exist
|
||||||
if (
|
if (googleApplicationCredentials && googleApplicationCredentialsContents) {
|
||||||
env('GOOGLE_APPLICATION_CREDENTIALS') &&
|
if (!fs.existsSync(googleApplicationCredentials)) {
|
||||||
env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS')
|
const contents = new Uint8Array(Buffer.from(googleApplicationCredentialsContents, 'base64'));
|
||||||
) {
|
|
||||||
if (!fs.existsSync(env('GOOGLE_APPLICATION_CREDENTIALS'))) {
|
|
||||||
const contents = new Uint8Array(
|
|
||||||
Buffer.from(env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS'), '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;
|
let cert: Buffer | null = null;
|
||||||
|
|
||||||
if (env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS')) {
|
const googleCloudHsmPublicCrtFileContents = env(
|
||||||
cert = Buffer.from(env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS'), 'base64');
|
'NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (googleCloudHsmPublicCrtFileContents) {
|
||||||
|
cert = Buffer.from(googleCloudHsmPublicCrtFileContents, 'base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cert) {
|
if (!cert) {
|
||||||
|
|||||||
@ -24,8 +24,10 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
|
|||||||
|
|
||||||
let cert: Buffer | null = null;
|
let cert: Buffer | null = null;
|
||||||
|
|
||||||
if (env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS')) {
|
const localFileContents = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS');
|
||||||
cert = Buffer.from(env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS'), 'base64');
|
|
||||||
|
if (localFileContents) {
|
||||||
|
cert = Buffer.from(localFileContents, 'base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cert) {
|
if (!cert) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { z } from 'zod';
|
|||||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { extractRequestMetadata } 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 = {
|
type CreateTrpcContextOptions = {
|
||||||
c: Context;
|
c: Context;
|
||||||
@ -58,7 +58,7 @@ export type TrpcContext = (
|
|||||||
user: null;
|
user: null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
session: unknown;
|
session: Session;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
) & {
|
) & {
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const t = initTRPC
|
|||||||
* Middlewares
|
* Middlewares
|
||||||
*/
|
*/
|
||||||
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
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`.
|
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
|
||||||
if (authorizationHeader) {
|
if (authorizationHeader) {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ type ComboBoxOption<T = OptionValue> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MultiSelectComboboxProps<T = OptionValue> = {
|
type MultiSelectComboboxProps<T = OptionValue> = {
|
||||||
emptySelectionPlaceholder?: React.ReactNode | string;
|
emptySelectionPlaceholder?: React.ReactElement | string;
|
||||||
enableClearAllButton?: boolean;
|
enableClearAllButton?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
inputPlaceholder?: MessageDescriptor;
|
inputPlaceholder?: MessageDescriptor;
|
||||||
|
|||||||
Reference in New Issue
Block a user