This commit is contained in:
David Nguyen
2025-01-31 14:09:02 +11:00
parent f7a98180d7
commit d7d0fca501
146 changed files with 1250 additions and 1263 deletions

View File

@ -7,8 +7,8 @@ import type { AuthAppType } from '../server';
import type {
TForgotPasswordSchema,
TResetPasswordSchema,
TSignInFormSchema,
TSignUpRequestSchema,
TSignInSchema,
TSignUpSchema,
TVerifyEmailSchema,
} from '../server/types/email-password';
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
@ -32,18 +32,6 @@ export class AuthClient {
return this.client.session.$get();
}
public passkey = {
signIn: async (data: TPasskeyAuthorizeSchema) => {
const result = await this.client['passkey'].authorize.$post({ json: data });
if (result.ok) {
return result.json();
}
throw new Error(result.statusText);
},
};
private async handleResponse<T>(response: ClientResponse<T>) {
if (!response.ok) {
const error = await response.json();
@ -59,9 +47,16 @@ export class AuthClient {
}
public emailPassword = {
signIn: async (data: TSignInFormSchema) => {
const response = await this.client['email-password'].authorize.$post({ json: data });
return this.handleResponse(response);
signIn: async (data: TSignInSchema & { redirectUrl?: string }) => {
const response = await this.client['email-password'].authorize
.$post({ json: data })
.then(this.handleResponse);
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
return response;
},
forgotPassword: async (data: TForgotPasswordSchema) => {
@ -74,7 +69,7 @@ export class AuthClient {
return this.handleResponse(response);
},
signUp: async (data: TSignUpRequestSchema) => {
signUp: async (data: TSignUpSchema) => {
const response = await this.client['email-password']['signup'].$post({ json: data });
return this.handleResponse(response);
},
@ -85,20 +80,29 @@ export class AuthClient {
},
};
public google = {
signIn: async () => {
const response = await this.client['google'].authorize.$post();
public passkey = {
signIn: async (data: TPasskeyAuthorizeSchema & { redirectUrl?: string }) => {
const response = await this.client['passkey'].authorize
.$post({ json: data })
.then(this.handleResponse);
// const parsedResponse = this.handleResponse(response);
if (!response.ok) {
const error = await response.json();
throw AppError.parseError(error);
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
const test = await response.json();
return response;
},
};
window.location.href = test.redirectUrl;
public google = {
signIn: async () => {
const response = await this.client['google'].authorize.$post().then(this.handleResponse);
if (response.redirectUrl) {
window.location.href = response.redirectUrl;
}
return response;
},
};
}

View File

@ -1,5 +1,8 @@
import { env } from '@documenso/lib/utils/env';
// Todo: Delete
export const authDebugger = (message: string) => {
if (process.env.NODE_ENV === 'development') {
if (env('NODE_ENV') === 'development') {
console.log(`[DEBUG]: ${message}`);
}
};

View File

@ -4,6 +4,7 @@ import { Hono } from 'hono';
import { DateTime } from 'luxon';
import { z } from 'zod';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
@ -16,19 +17,25 @@ import { createUser } from '@documenso/lib/server-only/user/create-user';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import {
EMAIL_VERIFICATION_STATE,
verifyEmail,
} from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
import { onAuthorize } from '../lib/utils/authorizer';
import { getRequiredSession } from '../lib/utils/get-session';
import { getRequiredSession, getSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context';
import {
ZForgotPasswordSchema,
ZResetPasswordSchema,
ZSignInFormSchema,
ZSignUpRequestSchema,
ZSignInSchema,
ZSignUpSchema,
ZUpdatePasswordSchema,
ZVerifyEmailSchema,
} from '../types/email-password';
@ -36,7 +43,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/**
* Authorize endpoint.
*/
.post('/authorize', zValidator('json', ZSignInFormSchema), async (c) => {
.post('/authorize', zValidator('json', ZSignInSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
const { email, password, totpCode, backupCode } = c.req.valid('json');
@ -48,8 +55,8 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
});
if (!user || !user.password) {
throw new AppError(AuthenticationErrorCode.NotFound, {
message: 'User not found',
throw new AppError(AuthenticationErrorCode.InvalidCredentials, {
message: 'Invalid email or password',
});
}
@ -85,7 +92,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
},
});
throw new AppError(AuthenticationErrorCode.IncorrectTwoFactorCode);
throw new AppError(AuthenticationErrorCode.InvalidTwoFactorCode);
}
}
@ -125,20 +132,20 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/**
* Signup endpoint.
*/
.post('/signup', zValidator('json', ZSignUpRequestSchema), async (c) => {
// if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
// throw new AppError('SIGNUP_DISABLED', {
// message: 'Signups are disabled.',
// });
// }
.post('/signup', zValidator('json', ZSignUpSchema), async (c) => {
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
throw new AppError('SIGNUP_DISABLED', {
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = c.req.valid('json');
// if (IS_BILLING_ENABLED() && url && url.length < 6) {
// throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
// message: 'Only subscribers can have a username shorter than 6 characters',
// });
// }
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError('PREMIUM_PROFILE_URL', {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
@ -149,18 +156,59 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
},
});
// Todo: Check this.
return c.json({
user,
return c.text('OK', 201);
})
/**
* Update password endpoint.
*/
.post('/update-password', zValidator('json', ZUpdatePasswordSchema), async (c) => {
const { password, currentPassword } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
const session = await getSession(c);
if (!session.isAuthenticated) {
throw new AppError(AuthenticationErrorCode.Unauthorized);
}
await updatePassword({
userId: session.user.id,
password,
currentPassword,
requestMetadata,
});
return c.text('OK', 201);
})
/**
* Verify email endpoint.
*/
.post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
await verifyEmail({ token: c.req.valid('json').token });
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
return c.text('OK', 201);
// If email is verified, automatically authenticate user.
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
await onAuthorize({ userId }, c);
}
return c.json({
state,
});
})
/**
* Resend verification email endpoint.
*/
.post('/resend-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
// If email is verified, automatically authenticate user.
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
await onAuthorize({ userId }, c);
}
return c.json({
state,
});
})
/**
* Forgot password endpoint.
@ -180,9 +228,12 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
await resetPassword({
token,
password,
requestMetadata,
});
return c.text('OK', 201);
@ -291,9 +342,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
requestMetadata,
});
return c.json({
success: true,
});
return c.text('OK', 201);
},
)
/**

View File

@ -4,6 +4,7 @@ import { getCookie, setCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
@ -12,8 +13,8 @@ import { getRequiredSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context';
const options = {
clientId: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID,
clientSecret: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
clientId: env('NEXT_PRIVATE_GOOGLE_CLIENT_ID'),
clientSecret: env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
redirectUri: 'http://localhost:3000/api/auth/google/callback',
scope: ['openid', 'email', 'profile'],
id: 'google',
@ -36,7 +37,7 @@ export const googleRoute = new Hono<HonoAuthContext>()
setCookie(c, 'google_oauth_state', state, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
secure: env('NODE_ENV') === 'production',
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
@ -44,7 +45,8 @@ export const googleRoute = new Hono<HonoAuthContext>()
setCookie(c, 'google_code_verifier', codeVerifier, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
// Todo: Might not be node_env but something vite specific?
secure: env('NODE_ENV') === 'production',
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});

View File

@ -5,14 +5,14 @@ export const ZCurrentPasswordSchema = z
.min(6, { message: 'Must be at least 6 characters in length' })
.max(72);
export const ZSignInFormSchema = z.object({
export const ZSignInSchema = z.object({
email: z.string().email().min(1),
password: ZCurrentPasswordSchema,
totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
});
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
export type TSignInSchema = z.infer<typeof ZSignInSchema>;
export const ZPasswordSchema = z
.string()
@ -31,7 +31,7 @@ export const ZPasswordSchema = z
message: 'One special character is required',
});
export const ZSignUpRequestSchema = z.object({
export const ZSignUpSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: ZPasswordSchema,
@ -47,7 +47,7 @@ export const ZSignUpRequestSchema = z.object({
.optional(),
});
export type TSignUpRequestSchema = z.infer<typeof ZSignUpRequestSchema>;
export type TSignUpSchema = z.infer<typeof ZSignUpSchema>;
export const ZForgotPasswordSchema = z.object({
email: z.string().email().min(1),
@ -67,3 +67,10 @@ export const ZVerifyEmailSchema = z.object({
});
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
export const ZUpdatePasswordSchema = z.object({
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
});
export type TUpdatePasswordSchema = z.infer<typeof ZUpdatePasswordSchema>;

View File

@ -8,6 +8,7 @@ import type { Stripe } from '@documenso/lib/server-only/stripe';
import { stripe } from '@documenso/lib/server-only/stripe';
import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { onSubscriptionDeleted } from './on-subscription-deleted';
@ -47,7 +48,7 @@ export const stripeWebhookHandler = async (
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET,
env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET'), // Todo: Test
);
await match(event.type)

View File

@ -1,6 +1,7 @@
import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer';
import { env } from '@documenso/lib/utils/env';
import { ResendTransport } from '@documenso/nodemailer-resend';
import { MailChannelsTransport } from './transports/mailchannels';
@ -51,13 +52,13 @@ import { MailChannelsTransport } from './transports/mailchannels';
* - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail.
*/
const getTransport = (): Transporter => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
const transport = env('NEXT_PRIVATE_SMTP_TRANSPORT') ?? 'smtp-auth';
if (transport === 'mailchannels') {
return createTransport(
MailChannelsTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_MAILCHANNELS_API_KEY,
endpoint: process.env.NEXT_PRIVATE_MAILCHANNELS_ENDPOINT,
apiKey: env('NEXT_PRIVATE_MAILCHANNELS_API_KEY'),
endpoint: env('NEXT_PRIVATE_MAILCHANNELS_ENDPOINT'),
}),
);
}
@ -65,43 +66,41 @@ const getTransport = (): Transporter => {
if (transport === 'resend') {
return createTransport(
ResendTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '',
}),
);
}
if (transport === 'smtp-api') {
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
if (!env('NEXT_PRIVATE_SMTP_HOST') || !env('NEXT_PRIVATE_SMTP_APIKEY')) {
throw new Error(
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
);
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST,
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
host: env('NEXT_PRIVATE_SMTP_HOST'),
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
auth: {
user: process.env.NEXT_PRIVATE_SMTP_APIKEY_USER ?? 'apikey',
pass: process.env.NEXT_PRIVATE_SMTP_APIKEY ?? '',
user: env('NEXT_PRIVATE_SMTP_APIKEY_USER') ?? 'apikey',
pass: env('NEXT_PRIVATE_SMTP_APIKEY') ?? '',
},
});
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? '127.0.0.1:2500',
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
ignoreTLS: process.env.NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS === 'true',
auth: process.env.NEXT_PRIVATE_SMTP_USERNAME
host: env('NEXT_PRIVATE_SMTP_HOST') ?? '127.0.0.1:2500',
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
ignoreTLS: env('NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS') === 'true',
auth: env('NEXT_PRIVATE_SMTP_USERNAME')
? {
user: process.env.NEXT_PRIVATE_SMTP_USERNAME,
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
user: env('NEXT_PRIVATE_SMTP_USERNAME'),
pass: env('NEXT_PRIVATE_SMTP_PASSWORD') ?? '',
}
: undefined,
...(process.env.NEXT_PRIVATE_SMTP_SERVICE
? { service: process.env.NEXT_PRIVATE_SMTP_SERVICE }
: {}),
...(env('NEXT_PRIVATE_SMTP_SERVICE') ? { service: env('NEXT_PRIVATE_SMTP_SERVICE') } : {}),
});
};

View File

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { Button, Column, Img, Link, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';

View File

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';

View File

@ -1,7 +1,9 @@
import { SentMessageInfo, Transport } from 'nodemailer';
import type { SentMessageInfo, Transport } from 'nodemailer';
import type { Address } from 'nodemailer/lib/mailer';
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
import { env } from '@documenso/lib/utils/env';
const VERSION = '1.0.0';
type NodeMailerAddress = string | Address | Array<string | Address> | undefined;
@ -79,9 +81,9 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
to: mailTo,
cc: mailCc.length > 0 ? mailCc : undefined,
bcc: mailBcc.length > 0 ? mailBcc : undefined,
dkim_domain: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN || undefined,
dkim_selector: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR || undefined,
dkim_private_key: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY || undefined,
dkim_domain: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN') || undefined,
dkim_selector: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR') || undefined,
dkim_private_key: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY') || undefined,
},
],
content: [

View File

@ -6,6 +6,7 @@ import {
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '../../constants/i18n';
import { env } from '../../utils/env';
import { remember } from '../../utils/remember';
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
@ -13,7 +14,7 @@ type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export async function loadCatalog(lang: SupportedLanguages): Promise<{
[k: string]: Messages;
}> {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
// const { messages } = await import(`../../translations/${lang}/web.${extension}`);
const messages = {};

View File

@ -1,16 +1,16 @@
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
// Todo: env('NEXT_PUBLIC_WEBAPP_URL')
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL();
env('NEXT_PRIVATE_INTERNAL_WEBAPP_URL') ?? NEXT_PUBLIC_WEBAPP_URL();
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
export const IS_APP_MARKETING = env('NEXT_PUBLIC_PROJECT') === 'marketing';
export const IS_APP_WEB = env('NEXT_PUBLIC_PROJECT') === 'web';
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const IS_APP_WEB_I18N_ENABLED = true;

View File

@ -1,4 +1,4 @@
// Todo: Reimport
import { env } from '../utils/env';
export const SALT_ROUNDS = 12;
@ -9,17 +9,16 @@ export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
};
export const IS_GOOGLE_SSO_ENABLED = Boolean(
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID &&
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
);
export const IS_OIDC_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_ID &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
);
export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL;
export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL');
export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
ACCOUNT_SSO_LINK: 'Linked account to SSO',
@ -49,7 +48,7 @@ export const PASSKEY_TIMEOUT = 60000;
export const MAXIMUM_PASSKEYS = 50;
export const useSecureCookies =
process.env.NODE_ENV === 'production' && String(process.env.NEXTAUTH_URL).startsWith('https://');
env('NODE_ENV') === 'production' && String(env('NEXTAUTH_URL')).startsWith('https://');
const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';

View File

@ -1,4 +1,6 @@
export const FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
export const FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
import { env } from '../utils/env';
export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com';
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';

View File

@ -1,4 +1,4 @@
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { APP_BASE_URL, WEBAPP_BASE_URL } from './app';

View File

@ -17,8 +17,6 @@ export enum AppErrorCode {
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
'PROFILE_URL_TAKEN' = 'PROFILE_URL_TAKEN',
'PREMIUM_PROFILE_URL' = 'PREMIUM_PROFILE_URL',
}
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
@ -34,8 +32,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
};
export const ZAppErrorJsonSchema = z.object({

View File

@ -1,5 +1,6 @@
import { match } from 'ts-pattern';
import { env } from '../../utils/env';
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
import type { BaseJobProvider as JobClientProvider } from './base';
import { InngestJobProvider } from './inngest';
@ -10,7 +11,7 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
private _provider: JobClientProvider;
public constructor(definitions: T) {
this._provider = match(process.env.NEXT_PRIVATE_JOBS_PROVIDER)
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
.with('inngest', () => InngestJobProvider.getInstance())
.with('trigger', () => TriggerJobProvider.getInstance())
.otherwise(() => LocalJobProvider.getInstance());

View File

@ -9,6 +9,7 @@ import type { Logger } from 'inngest/middleware/logger';
import { serve as createPagesRoute } from 'inngest/next';
import { json } from 'micro';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base';
@ -28,8 +29,8 @@ export class InngestJobProvider extends BaseJobProvider {
static getInstance() {
if (!this._instance) {
const client = new InngestClient({
id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app',
eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
});
this._instance = new InngestJobProvider({ client });

View File

@ -2,6 +2,7 @@ import { createPagesRoute } from '@trigger.dev/nextjs';
import type { IO } from '@trigger.dev/sdk';
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base';
@ -20,8 +21,8 @@ export class TriggerJobProvider extends BaseJobProvider {
if (!this._instance) {
const client = new TriggerClient({
id: 'documenso-app',
apiKey: process.env.NEXT_PRIVATE_TRIGGER_API_KEY,
apiUrl: process.env.NEXT_PRIVATE_TRIGGER_API_URL,
apiKey: env('NEXT_PRIVATE_TRIGGER_API_KEY'),
apiUrl: env('NEXT_PRIVATE_TRIGGER_API_URL'),
});
this._instance = new TriggerJobProvider({ client });

View File

@ -10,8 +10,8 @@ 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 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';

View File

@ -1,24 +1,28 @@
export const isErrorCode = (code: unknown): code is ErrorCode => {
return typeof code === 'string' && code in ErrorCode;
};
// export const isErrorCode = (code: unknown): code is ErrorCode => {
// return typeof code === 'string' && code in ErrorCode;
// };
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
// export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
export const ErrorCode = {
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
} as const;
// Todo: Delete file
// Todo: Delete file
// Todo: Delete file
// Todo: Delete file
// export const ErrorCode = {
// INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
// USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
// CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
// INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
// TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
// TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
// TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
// TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
// INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
// INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
// INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
// INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
// MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
// MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
// UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
// ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
// } as const;

View File

@ -3,7 +3,6 @@ import { base32 } from '@scure/base';
import crypto from 'crypto';
import { createTOTPKeyURI } from 'oslo/otp';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
@ -21,7 +20,7 @@ export const setupTwoFactorAuthentication = async ({
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
throw new Error('MISSING_ENCRYPTION_KEY');
}
const secret = crypto.randomBytes(10);

View File

@ -1,6 +1,6 @@
import type { User } from '@prisma/client';
import { ErrorCode } from '../../next-auth/error-codes';
import { AppError } from '../../errors/app-error';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
import { verifyBackupCode } from './verify-backup-code';
@ -16,11 +16,11 @@ export const validateTwoFactorAuthentication = async ({
user,
}: ValidateTwoFactorAuthenticationOptions) => {
if (!user.twoFactorEnabled) {
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED);
throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
}
if (!user.twoFactorSecret) {
throw new Error(ErrorCode.TWO_FACTOR_MISSING_SECRET);
throw new AppError('TWO_FACTOR_MISSING_SECRET');
}
if (totpCode) {
@ -31,5 +31,5 @@ export const validateTwoFactorAuthentication = async ({
return await verifyBackupCode({ user, backupCode });
}
throw new Error(ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS);
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
};

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
@ -15,8 +16,8 @@ export interface SendConfirmationEmailProps {
}
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
const NEXT_PRIVATE_SMTP_FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME;
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS;
const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
const user = await prisma.user.findFirstOrThrow({
where: {

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendForgotPasswordOptions {
@ -55,8 +56,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Forgot Password?`),
html,

View File

@ -5,6 +5,7 @@ import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password
import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendResetPasswordOptions {
@ -37,8 +38,8 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: 'Password Reset Success!',
html,

View File

@ -14,6 +14,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -113,8 +114,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Signing Complete!`),
html,
@ -190,8 +191,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject:
isDirectTemplate && document.documentMeta?.subject

View File

@ -10,6 +10,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -79,8 +80,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
name: name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Document Deleted!`),
html,

View File

@ -9,6 +9,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -90,8 +91,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Waiting for others to complete signing.`),
html,

View File

@ -3,6 +3,7 @@ import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { env } from '../../utils/env';
import { encryptSecondaryData } from '../crypto/encrypt';
export type GetCertificatePdfOptions = {
@ -21,10 +22,10 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
let browser: Browser;
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
if (env('NEXT_PRIVATE_BROWSERLESS_URL')) {
// !: 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(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
browser = await chromium.connectOverCDP(env('NEXT_PRIVATE_BROWSERLESS_URL'));
} else {
browser = await chromium.launch();
}

View File

@ -25,15 +25,12 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta';
import { env } from '../../utils/env';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
res.arrayBuffer(),
);
const fontCaveat = await fetch(env('FONT_CAVEAT_URI')).then(async (res) => res.arrayBuffer());
const fontNoto = await fetch(process.env.FONT_NOTO_SANS_URI).then(async (res) =>
res.arrayBuffer(),
);
const fontNoto = await fetch(env('FONT_NOTO_SANS_URI')).then(async (res) => res.arrayBuffer());
const isSignatureField = isSignatureFieldType(field.type);

View File

@ -1,8 +1,10 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { Redis } from '@upstash/redis';
import { env } from '../../utils/env';
// !: We're null coalescing here because we don't want local builds to fail.
export const redis = new Redis({
url: process.env.NEXT_PRIVATE_REDIS_URL ?? '',
token: process.env.NEXT_PRIVATE_REDIS_TOKEN ?? '',
url: env('NEXT_PRIVATE_REDIS_URL') ?? '',
token: env('NEXT_PRIVATE_REDIS_TOKEN') ?? '',
});

View File

@ -1,7 +1,9 @@
/// <reference types="./stripe.d.ts" />
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
import { env } from '../../utils/env';
export const stripe = new Stripe(env('NEXT_PRIVATE_STRIPE_API_KEY') ?? '', {
apiVersion: '2022-11-15',
typescript: true,
});

View File

@ -15,6 +15,7 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -122,7 +123,7 @@ export const sendTeamEmailVerificationEmail = async (
teamGlobalSettings?: TeamGlobalSettings | null;
},
) => {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
const template = createElement(ConfirmTeamEmailTemplate, {
assetBaseUrl,

View File

@ -10,6 +10,7 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -69,7 +70,7 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
});
try {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
const template = createElement(TeamEmailRemovedTemplate, {
assetBaseUrl,

View File

@ -44,6 +44,7 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { formatDocumentsPath } from '../../utils/teams';
@ -582,8 +583,8 @@ export const createDocumentFromDirectTemplate = async ({
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Document created from direct template`),
html,

View File

@ -1,5 +1,5 @@
import { hash } from '@node-rs/bcrypt';
import { IdentityProvider, TeamMemberInviteStatus } from '@prisma/client';
import { TeamMemberInviteStatus } from '@prisma/client';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
@ -39,22 +39,35 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
});
if (urlExists) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
throw new AppError('PROFILE_URL_TAKEN', {
message: 'Profile username is taken',
userMessage: 'The profile username is already taken',
});
}
}
const user = await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
url,
},
const user = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword, // Todo: Drop password.
signature,
url,
},
});
await tx.account.create({
data: {
userId: user.id,
type: 'emailPassword', // Todo
provider: 'DOCUMENSO', // Todo: Enums
providerAccountId: user.id.toString(),
password: hashedPassword,
},
});
return user;
});
const acceptedTeamInvites = await prisma.teamMemberInvite.findMany({

View File

@ -26,7 +26,10 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
if (!verificationToken) {
return EMAIL_VERIFICATION_STATE.NOT_FOUND;
return {
state: EMAIL_VERIFICATION_STATE.NOT_FOUND,
userId: null,
};
}
// check if the token is valid or expired
@ -55,11 +58,17 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
}
return EMAIL_VERIFICATION_STATE.EXPIRED;
return {
state: EMAIL_VERIFICATION_STATE.EXPIRED,
userId: null,
};
}
if (verificationToken.completed) {
return EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED,
userId: null,
};
}
const [updatedUser] = await prisma.$transaction([
@ -94,5 +103,8 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
throw new Error('Something went wrong while verifying your email. Please try again.');
}
return EMAIL_VERIFICATION_STATE.VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.VERIFIED,
userId: updatedUser.id,
};
};

View File

@ -1,7 +1,7 @@
{
"extends": "@documenso/tsconfig/react-library.json",
"compilerOptions": {
"types": ["@documenso/tsconfig/process-env.d.ts"],
"types": ["@documenso/tsconfig/process-env.d.ts", "vite/client"],
"moduleResolution": "Bundler"
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],

View File

@ -1,13 +1,14 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
import { env } from '../utils/env';
export const getBaseUrl = () => {
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
if (env('VERCEL_URL')) {
return `https://${env('VERCEL_URL')}`;
}
const webAppUrl = NEXT_PUBLIC_WEBAPP_URL();
@ -16,5 +17,5 @@ export const getBaseUrl = () => {
return webAppUrl;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
return `http://localhost:${env('PORT') ?? 3000}`;
};

View File

@ -1,9 +1,10 @@
import { DocumentDataType } from '@prisma/client';
import { base64 } from '@scure/base';
import { env } from 'next-runtime-env';
import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
import { env } from '@documenso/lib/utils/env';
import { AppError } from '../../errors/app-error';
import { createDocumentData } from '../../server-only/document-data/create-document-data';

View File

@ -1,8 +1,3 @@
'use server';
import { headers } from 'next/headers';
import { NextRequest } from 'next/server';
import {
DeleteObjectCommand,
GetObjectCommand,
@ -10,10 +5,11 @@ import {
S3Client,
} from '@aws-sdk/client-s3';
import slugify from '@sindresorhus/slugify';
import { type JWT, getToken } from 'next-auth/jwt';
import { env } from 'next-runtime-env';
import { type JWT } from 'next-auth/jwt';
import path from 'node:path';
import { env } from '@documenso/lib/utils/env';
import { APP_BASE_URL } from '../../constants/app';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { alphaid } from '../id';
@ -23,16 +19,17 @@ export const getPresignPostUrl = async (fileName: string, contentType: string) =
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
let token: JWT | null = null;
const token: JWT | null = null;
try {
const baseUrl = APP_BASE_URL() ?? 'http://localhost:3000';
token = await getToken({
req: new NextRequest(baseUrl, {
headers: headers(),
}),
});
// Todo
// token = await getToken({
// req: new NextRequest(baseUrl, {
// headers: headers(),
// }),
// });
} catch (err) {
// Non server-component environment
}
@ -47,7 +44,7 @@ export const getPresignPostUrl = async (fileName: string, contentType: string) =
}
const putObjectCommand = new PutObjectCommand({
Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET,
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
ContentType: contentType,
});
@ -65,7 +62,7 @@ export const getAbsolutePresignPostUrl = async (key: string) => {
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
const putObjectCommand = new PutObjectCommand({
Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET,
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
@ -77,15 +74,15 @@ export const getAbsolutePresignPostUrl = async (key: string) => {
};
export const getPresignGetUrl = async (key: string) => {
if (process.env.NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN) {
const distributionUrl = new URL(key, `${process.env.NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN}`);
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
const url = getCloudfrontSignedUrl({
url: distributionUrl.toString(),
keyPairId: `${process.env.NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID}`,
privateKey: `${process.env.NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS}`,
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
});
@ -97,7 +94,7 @@ export const getPresignGetUrl = async (key: string) => {
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
const getObjectCommand = new GetObjectCommand({
Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET,
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
@ -113,7 +110,7 @@ export const deleteS3File = async (key: string) => {
await client.send(
new DeleteObjectCommand({
Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET,
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
}),
);
@ -127,17 +124,16 @@ const getS3Client = () => {
}
const hasCredentials =
process.env.NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID &&
process.env.NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY;
env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
return new S3Client({
endpoint: process.env.NEXT_PRIVATE_UPLOAD_ENDPOINT || undefined,
forcePathStyle: process.env.NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE === 'true',
region: process.env.NEXT_PRIVATE_UPLOAD_REGION || 'us-east-1',
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
credentials: hasCredentials
? {
accessKeyId: String(process.env.NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID),
secretAccessKey: String(process.env.NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY),
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
}
: undefined,
});

18
packages/lib/utils/env.ts Normal file
View File

@ -0,0 +1,18 @@
/// <reference types="@documenso/tsconfig/process-env.d.ts" />
type EnvironmentVariable = keyof NodeJS.ProcessEnv;
export const env = (variable: EnvironmentVariable | (string & {})) => {
// console.log({
// ['typeof window']: typeof window,
// ['process.env']: process.env,
// ['window.__ENV__']: typeof window !== 'undefined' && window.__ENV__,
// });
// This may need ot be import.meta.env.SSR depending on vite.
if (typeof window !== 'undefined' && typeof window.__ENV__ === 'object') {
return window.__ENV__[variable];
}
return process.env[variable];
};

View File

@ -5,9 +5,10 @@ import type { I18n, MessageDescriptor } from '@lingui/core';
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../constants/app';
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
import { APP_I18N_OPTIONS } from '../constants/i18n';
import { env } from './env';
export async function dynamicActivate(i18nInstance: I18n, locale: string) {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
// const { messages } = await import(`../translations/${locale}/web.${extension}`);
// todo

View File

@ -1,7 +1,9 @@
import Honeybadger from '@honeybadger-io/js';
import { env } from './env';
export const buildLogger = () => {
if (process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
return new HoneybadgerLogger();
}
@ -45,12 +47,12 @@ class DefaultLogger implements Logger {
class HoneybadgerLogger implements Logger {
constructor() {
if (!process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
if (!env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
}
Honeybadger.configure({
apiKey: process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY,
apiKey: env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY'),
});
}

View File

@ -23,33 +23,26 @@ datasource db {
directUrl = env("NEXT_PRIVATE_DIRECT_DATABASE_URL")
}
enum IdentityProvider {
DOCUMENSO
GOOGLE
OIDC
}
enum Role {
ADMIN
USER
}
model User {
id Int @id @default(autoincrement())
name String?
customerId String? @unique
email String @unique
emailVerified DateTime?
password String?
source String?
signature String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
lastSignedIn DateTime @default(now())
roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO)
avatarImageId String?
disabled Boolean @default(false)
id Int @id @default(autoincrement())
name String?
customerId String? @unique
email String @unique
emailVerified DateTime?
password String?
source String?
signature String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
lastSignedIn DateTime @default(now())
roles Role[] @default([USER])
avatarImageId String?
disabled Boolean @default(false)
accounts Account[]
sessions Session[]

View File

@ -1,5 +1,7 @@
import { match } from 'ts-pattern';
import { env } from '@documenso/lib/utils/env';
import { signWithGoogleCloudHSM } from './transports/google-cloud-hsm';
import { signWithLocalCert } from './transports/local-cert';
@ -8,7 +10,7 @@ export type SignOptions = {
};
export const signPdf = async ({ pdf }: SignOptions) => {
const transport = process.env.NEXT_PRIVATE_SIGNING_TRANSPORT || 'local';
const transport = env('NEXT_PRIVATE_SIGNING_TRANSPORT') || 'local';
return await match(transport)
.with('local', async () => signWithLocalCert({ pdf }))

View File

@ -1,5 +1,6 @@
import fs from 'node:fs';
import { env } from '@documenso/lib/utils/env';
import { signWithGCloud } from '@documenso/pdf-sign';
import { addSigningPlaceholder } from '../helpers/add-signing-placeholder';
@ -10,7 +11,7 @@ export type SignWithGoogleCloudHSMOptions = {
};
export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOptions) => {
const keyPath = process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH;
const keyPath = env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH');
if (!keyPath) {
throw new Error('No certificate path provided for Google Cloud HSM signing');
@ -19,18 +20,15 @@ export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOpti
// 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 (
process.env.GOOGLE_APPLICATION_CREDENTIALS &&
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS
env('GOOGLE_APPLICATION_CREDENTIALS') &&
env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS')
) {
if (!fs.existsSync(process.env.GOOGLE_APPLICATION_CREDENTIALS)) {
if (!fs.existsSync(env('GOOGLE_APPLICATION_CREDENTIALS'))) {
const contents = new Uint8Array(
Buffer.from(
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS,
'base64',
),
Buffer.from(env('NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS'), 'base64'),
);
fs.writeFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, contents);
fs.writeFileSync(env('GOOGLE_APPLICATION_CREDENTIALS'), contents);
}
}
@ -47,17 +45,14 @@ export const signWithGoogleCloudHSM = async ({ pdf }: SignWithGoogleCloudHSMOpti
let cert: Buffer | null = null;
if (process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS) {
cert = Buffer.from(
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS,
'base64',
);
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');
}
if (!cert) {
cert = Buffer.from(
fs.readFileSync(
process.env.NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH || './example/cert.crt',
env('NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH') || './example/cert.crt',
),
);
}

View File

@ -1,5 +1,6 @@
import fs from 'node:fs';
import { env } from '@documenso/lib/utils/env';
import { signWithP12 } from '@documenso/pdf-sign';
import { addSigningPlaceholder } from '../helpers/add-signing-placeholder';
@ -23,20 +24,20 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
let cert: Buffer | null = null;
if (process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS) {
cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
if (env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS')) {
cert = Buffer.from(env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS'), 'base64');
}
if (!cert) {
let certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || '/opt/documenso/cert.p12';
let certPath = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH') || '/opt/documenso/cert.p12';
// We don't want to make the development server suddenly crash when using the `dx` script
// so we retain this when NODE_ENV isn't set to production which it should be in most production
// deployments.
//
// Our docker image automatically sets this so it shouldn't be an issue for self-hosters.
if (process.env.NODE_ENV !== 'production') {
certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12';
if (env('NODE_ENV') !== 'production') {
certPath = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH') || './example/cert.p12';
}
cert = Buffer.from(fs.readFileSync(certPath));
@ -45,7 +46,7 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
const signature = signWithP12({
cert,
content: pdfWithoutSignature,
password: process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE || undefined,
password: env('NEXT_PRIVATE_SIGNING_PASSPHRASE') || undefined,
});
const signatureAsHex = signature.toString('hex');

View File

@ -1,5 +1,3 @@
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@ -7,6 +5,8 @@ import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
// import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router';
@ -36,10 +36,6 @@ export interface TrpcProviderProps {
headers?: Record<string, string>;
}
// 'next-runtime-env
// Todo
const getBaseUrl = () => 'http://localhost:3000';
export function TrpcProvider({ children, headers }: TrpcProviderProps) {
const [queryClient] = useState(() => new QueryClient());

View File

@ -1,9 +1,5 @@
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
import { env } from 'next-runtime-env';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
import { createPasskeyAuthenticationOptions } from '@documenso/lib/server-only/auth/create-passkey-authentication-options';
import { createPasskeyRegistrationOptions } from '@documenso/lib/server-only/auth/create-passkey-registration-options';
@ -11,7 +7,6 @@ import { createPasskeySigninOptions } from '@documenso/lib/server-only/auth/crea
import { deletePasskey } from '@documenso/lib/server-only/auth/delete-passkey';
import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { nanoid } from '@documenso/lib/universal/id';
import { authenticatedProcedure, procedure, router } from '../trpc';
@ -20,40 +15,10 @@ import {
ZCreatePasskeyMutationSchema,
ZDeletePasskeyMutationSchema,
ZFindPasskeysQuerySchema,
ZSignUpMutationSchema,
ZUpdatePasskeyMutationSchema,
} from './schema';
const NEXT_PUBLIC_DISABLE_SIGNUP = () => env('NEXT_PUBLIC_DISABLE_SIGNUP');
export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new AppError('SIGNUP_DISABLED', {
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
return user;
}),
createPasskey: authenticatedProcedure
.input(ZCreatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {

View File

@ -1,28 +1,20 @@
import { SubscriptionStatus } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
import { AppError } from '@documenso/lib/errors/app-error';
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
import {
ZConfirmEmailMutationSchema,
ZFindUserSecurityAuditLogsSchema,
ZForgotPasswordFormSchema,
ZResetPasswordFormSchema,
ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
ZUpdatePublicProfileMutationSchema,
} from './schema';
@ -69,7 +61,7 @@ export const profileRouter = router({
);
if (subscriptions.length === 0) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
throw new AppError('PREMIUM_PROFILE_URL', {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
@ -87,50 +79,6 @@ export const profileRouter = router({
return { success: true, url: user.url };
}),
updatePassword: authenticatedProcedure
.input(ZUpdatePasswordMutationSchema)
.mutation(async ({ input, ctx }) => {
const { password, currentPassword } = input;
return await updatePassword({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => {
const { email } = input;
return await forgotPassword({
email,
});
}),
resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input, ctx }) => {
const { password, token } = input;
return await resetPassword({
token,
password,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
sendConfirmationEmail: procedure
.input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => {
const { email } = input;
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email,
},
});
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
return await deleteUser({
id: ctx.user.id,

View File

@ -1,7 +1,5 @@
import { z } from 'zod';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '../auth-router/schema';
export const MAX_PROFILE_BIO_LENGTH = 256;
export const ZFindUserSecurityAuditLogsSchema = z.object({
@ -45,32 +43,6 @@ export const ZUpdatePublicProfileMutationSchema = z.object({
export type TUpdatePublicProfileMutationSchema = z.infer<typeof ZUpdatePublicProfileMutationSchema>;
export const ZUpdatePasswordMutationSchema = z.object({
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
});
export type TUpdatePasswordMutationSchema = z.infer<typeof ZUpdatePasswordMutationSchema>;
export const ZForgotPasswordFormSchema = z.object({
email: z.string().email().min(1),
});
export type TForgotPasswordFormSchema = z.infer<typeof ZForgotPasswordFormSchema>;
export const ZResetPasswordFormSchema = z.object({
password: ZPasswordSchema,
token: z.string().min(1),
});
export type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
export const ZConfirmEmailMutationSchema = z.object({
email: z.string().email().min(1),
});
export type TConfirmEmailMutationSchema = z.infer<typeof ZConfirmEmailMutationSchema>;
export const ZSetProfileImageMutationSchema = z.object({
bytes: z.string().nullish(),
teamId: z.number().min(1).nullish(),

View File

@ -2,11 +2,10 @@
import { useState } from 'react';
import type { DocumentData } from '@prisma/client';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import type { DocumentData } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
import { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';

View File

@ -5,10 +5,10 @@ import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { DocumentData } from '@prisma/client';
import { Download } from 'lucide-react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { DocumentData } from '@documenso/prisma/client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Button } from '../../primitives/button';

View File

@ -1,3 +1,4 @@
import type { Field } from '@prisma/client';
import { TooltipArrow } from '@radix-ui/react-tooltip';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
@ -12,7 +13,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from '../..//primitives/tooltip';
import type { Field } from '.prisma/client';
const tooltipVariants = cva('font-semibold', {
variants: {

View File

@ -2,12 +2,12 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { Field } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import type { Field } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
import { Card, CardContent } from '../../primitives/card';

View File

@ -3,10 +3,10 @@
import React, { forwardRef } from 'react';
import { Trans } from '@lingui/macro';
import { RecipientRole } from '@prisma/client';
import type { SelectProps } from '@radix-ui/react-select';
import { InfoIcon } from 'lucide-react';
import { RecipientRole } from '@documenso/prisma/client';
import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';

View File

@ -5,11 +5,10 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import type { StaticImageData } from 'next/image';
import Image from 'next/image';
import type { Signature } from '@prisma/client';
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
import { P, match } from 'ts-pattern';
import type { Signature } from '@documenso/prisma/client';
import { cn } from '../lib/utils';
import { Card, CardContent } from '../primitives/card';

View File

@ -1,12 +1,12 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
// Todo
// import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Prisma } from '@prisma/client';
import type { Field, Recipient } from '@prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
import {
CalendarDays,
Check,
@ -41,8 +41,6 @@ import {
canRecipientBeModified,
canRecipientFieldsBeModified,
} from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { FieldToolTip } from '../../components/field/field-tooltip';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
@ -70,12 +68,12 @@ import { FieldAdvancedSettings } from './field-item-advanced-settings';
import { MissingSignatureFieldDialog } from './missing-signature-field-dialog';
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
// const fontCaveat = Caveat({
// weight: ['500'],
// subsets: ['latin'],
// display: 'swap',
// variable: '--font-caveat',
// });
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@ -612,7 +610,7 @@ export const AddFieldsFormPartial = ({
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
'dark:text-black/60': isFieldWithinBounds,
},
selectedField === FieldType.SIGNATURE && fontCaveat.className,
// selectedField === FieldType.SIGNATURE && fontCaveat.className,
)}
style={{
top: coords.y,
@ -835,7 +833,7 @@ export const AddFieldsFormPartial = ({
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
fontCaveat.className,
// fontCaveat.className,
)}
>
<Trans>Signature</Trans>

View File

@ -1,7 +1,7 @@
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZAddFieldsFormSchema = z.object({
fields: z.array(

View File

@ -4,6 +4,8 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@ -13,8 +15,6 @@ import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import type { TDocument } from '@documenso/lib/types/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
import {
DocumentGlobalAuthAccessSelect,
DocumentGlobalAuthAccessTooltip,

View File

@ -1,3 +1,4 @@
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -8,7 +9,6 @@ import {
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentVisibility } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,

View File

@ -7,6 +7,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { Field, Recipient } from '@prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
@ -17,8 +19,6 @@ import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';

View File

@ -1,10 +1,10 @@
import { msg } from '@lingui/macro';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
export const ZAddSignersFormSchema = z
.object({

View File

@ -3,6 +3,8 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { Field, Recipient } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form';
@ -10,12 +12,6 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import {
DocumentDistributionMethod,
DocumentStatus,
RecipientRole,
} from '@documenso/prisma/client';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';

View File

@ -1,9 +1,8 @@
import { DocumentDistributionMethod } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentDistributionMethod } from '.prisma/client';
export const ZAddSubjectFormSchema = z.object({
meta: z.object({
subject: z.string(),

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/macro';
import { FieldType } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
@ -12,7 +13,6 @@ import {
} from 'lucide-react';
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';

View File

@ -5,6 +5,7 @@ import { forwardRef, useEffect, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { FieldType } from '@prisma/client';
import { match } from 'ts-pattern';
import {
@ -21,7 +22,6 @@ import {
type TTextFieldMeta as TextFieldMeta,
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import type { FieldFormType } from './add-fields';

View File

@ -1,9 +1,7 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
// Todo
// import { Caveat } from 'next/font/google';
import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
@ -22,12 +20,12 @@ import type { TDocumentFlowFormSchema } from './types';
type Field = TDocumentFlowFormSchema['fields'][0];
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
// const fontCaveat = Caveat({
// weight: ['500'],
// subsets: ['latin'],
// display: 'swap',
// variable: '--font-caveat',
// });
export type FieldItemProps = {
field: Field;
@ -258,7 +256,7 @@ export const FieldItem = ({
fieldMeta={field.fieldMeta}
type={field.type}
signerEmail={field.signerEmail}
fontCaveatClassName={fontCaveat.className}
// fontCaveatClassName={fontCaveat.className}
/>
))}

View File

@ -1,25 +1,21 @@
'use client';
import { Caveat } from 'next/font/google';
// import { Caveat } from 'next/font/google';
import { useLingui } from '@lingui/react';
import type { Prisma } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { FieldType } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
import { Card, CardContent } from '../card';
import { FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
// const fontCaveat = Caveat({
// weight: ['500'],
// subsets: ['latin'],
// display: 'swap',
// variable: '--font-caveat',
// });
export type ShowFieldItemProps = {
field: Prisma.FieldGetPayload<null>;
@ -48,7 +44,7 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
<CardContent
className={cn(
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
field.type === FieldType.SIGNATURE && fontCaveat.className,
// field.type === FieldType.SIGNATURE && fontCaveat.className,
)}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}

View File

@ -1,9 +1,9 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZDocumentFlowFormSchema = z.object({
title: z.string().min(1),

View File

@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { DocumentData } from '@prisma/client';
import { Loader } from 'lucide-react';
import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
@ -13,7 +14,6 @@ import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import type { DocumentData } from '@documenso/prisma/client';
import { cn } from '../lib/utils';
import { PasswordDialog } from './document-password-dialog';

View File

@ -1,7 +1,6 @@
import type { RecipientRole } from '@prisma/client';
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
import type { RecipientRole } from '.prisma/client';
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
SIGNER: <PencilLine className="h-4 w-4" />,
APPROVER: <BadgeCheck className="h-4 w-4" />,

View File

@ -1,11 +1,11 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
// Todo
// import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { Field, Recipient } from '@prisma/client';
import { FieldType, RecipientRole } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
@ -31,8 +31,6 @@ import {
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -64,12 +62,12 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form'
import { useStep } from '../stepper';
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
// const fontCaveat = Caveat({
// weight: ['500'],
// subsets: ['latin'],
// display: 'swap',
// variable: '--font-caveat',
// });
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@ -700,7 +698,7 @@ export const AddTemplateFieldsFormPartial = ({
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
fontCaveat.className,
// fontCaveat.className,
)}
>
<Trans>Signature</Trans>

View File

@ -1,7 +1,7 @@
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZAddTemplateFieldsFormSchema = z.object({
fields: z.array(

View File

@ -7,6 +7,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { TemplateDirectLink } from '@prisma/client';
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
@ -15,13 +17,6 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
import type { TemplateDirectLink } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
type Field,
type Recipient,
RecipientRole,
} from '@documenso/prisma/client';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';

View File

@ -1,7 +1,7 @@
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/client';
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';

View File

@ -5,6 +5,8 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@ -16,8 +18,6 @@ import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
import {
DocumentGlobalAuthAccessSelect,

View File

@ -1,3 +1,5 @@
import { DocumentDistributionMethod } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -9,14 +11,12 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentVisibility } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '@documenso/trpc/server/document-router/schema';
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
import { DocumentDistributionMethod } from '.prisma/client';
export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),