mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
wip
This commit is contained in:
112
packages/auth/client/index.ts
Normal file
112
packages/auth/client/index.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import type { ClientResponse } from 'hono/client';
|
||||
import { hc } from 'hono/client';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import type { AuthAppType } from '../server';
|
||||
import type {
|
||||
TForgotPasswordSchema,
|
||||
TResetPasswordSchema,
|
||||
TSignInFormSchema,
|
||||
TSignUpRequestSchema,
|
||||
TVerifyEmailSchema,
|
||||
} from '../server/types/email-password';
|
||||
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
|
||||
|
||||
export class AuthClient {
|
||||
public client: ReturnType<typeof hc<AuthAppType>>;
|
||||
|
||||
private signOutRedirectUrl: string = '/signin';
|
||||
|
||||
constructor(options: { baseUrl: string }) {
|
||||
this.client = hc<AuthAppType>(options.baseUrl);
|
||||
}
|
||||
|
||||
public async signOut() {
|
||||
await this.client.signout.$post();
|
||||
|
||||
window.location.href = this.signOutRedirectUrl;
|
||||
}
|
||||
|
||||
public async session() {
|
||||
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();
|
||||
|
||||
throw AppError.parseError(error);
|
||||
}
|
||||
|
||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
public emailPassword = {
|
||||
signIn: async (data: TSignInFormSchema) => {
|
||||
const response = await this.client['email-password'].authorize.$post({ json: data });
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
forgotPassword: async (data: TForgotPasswordSchema) => {
|
||||
const response = await this.client['email-password']['forgot-password'].$post({ json: data });
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
resetPassword: async (data: TResetPasswordSchema) => {
|
||||
const response = await this.client['email-password']['reset-password'].$post({ json: data });
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
signUp: async (data: TSignUpRequestSchema) => {
|
||||
const response = await this.client['email-password']['signup'].$post({ json: data });
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
verifyEmail: async (data: TVerifyEmailSchema) => {
|
||||
const response = await this.client['email-password']['verify-email'].$post({ json: data });
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
};
|
||||
|
||||
public google = {
|
||||
signIn: async () => {
|
||||
const response = await this.client['google'].authorize.$post();
|
||||
|
||||
// const parsedResponse = this.handleResponse(response);
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
throw AppError.parseError(error);
|
||||
}
|
||||
|
||||
const test = await response.json();
|
||||
|
||||
window.location.href = test.redirectUrl;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: Env
|
||||
// Todo: Remove in favor of AuthClient
|
||||
// export const authClient = hc<AuthAppType>('http://localhost:3000/api/auth');
|
||||
|
||||
export const authClient = new AuthClient({
|
||||
baseUrl: 'http://localhost:3000/api/auth',
|
||||
});
|
||||
2
packages/auth/index.ts
Normal file
2
packages/auth/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './server/lib/errors/errors';
|
||||
export * from './server/lib/errors/error-codes';
|
||||
25
packages/auth/package.json
Normal file
25
packages/auth/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@documenso/auth",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@hono/zod-validator": "^0.4.2",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"arctic": "^3.1.0",
|
||||
"hono": "4.6.15",
|
||||
"luxon": "^3.5.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "3.24.1"
|
||||
}
|
||||
}
|
||||
72
packages/auth/server/index.ts
Normal file
72
packages/auth/server/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
|
||||
import { emailPasswordRoute } from './routes/email-password';
|
||||
import { googleRoute } from './routes/google';
|
||||
import { passkeyRoute } from './routes/passkey';
|
||||
import { sessionRoute } from './routes/session';
|
||||
import { signOutRoute } from './routes/sign-out';
|
||||
import type { HonoAuthContext } from './types/context';
|
||||
|
||||
// Note: You must chain routes for Hono RPC client to work.
|
||||
export const auth = new Hono<HonoAuthContext>()
|
||||
.use(async (c, next) => {
|
||||
c.set('requestMetadata', extractRequestMetadata(c.req.raw));
|
||||
await next();
|
||||
})
|
||||
.route('/', sessionRoute)
|
||||
.route('/', signOutRoute)
|
||||
.route('/email-password', emailPasswordRoute)
|
||||
.route('/passkey', passkeyRoute)
|
||||
.route('/google', googleRoute);
|
||||
|
||||
/**
|
||||
* Handle errors.
|
||||
*/
|
||||
auth.onError((err, c) => {
|
||||
console.error(`-----------`);
|
||||
console.error(`-----------`);
|
||||
console.error(`-----------`);
|
||||
console.error(`${err}`);
|
||||
|
||||
if (err instanceof HTTPException) {
|
||||
return c.json(
|
||||
{
|
||||
code: AppErrorCode.UNKNOWN_ERROR,
|
||||
message: err.message,
|
||||
statusCode: err.status,
|
||||
},
|
||||
err.status,
|
||||
);
|
||||
}
|
||||
|
||||
if (err instanceof AppError) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const statusCode = (err.statusCode || 500) as ContentfulStatusCode;
|
||||
|
||||
return c.json(
|
||||
{
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
statusCode: err.statusCode,
|
||||
},
|
||||
statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return c.json(
|
||||
{
|
||||
code: AppErrorCode.UNKNOWN_ERROR,
|
||||
message: 'Internal Server Error',
|
||||
statusCode: 500,
|
||||
},
|
||||
500,
|
||||
);
|
||||
});
|
||||
|
||||
export type AuthAppType = typeof auth;
|
||||
29
packages/auth/server/lib/errors/error-codes.ts
Normal file
29
packages/auth/server/lib/errors/error-codes.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export const AuthenticationErrorCode = {
|
||||
AccountDisabled: 'ACCOUNT_DISABLED',
|
||||
Unauthorized: 'UNAUTHORIZED',
|
||||
InvalidCredentials: 'INVALID_CREDENTIALS',
|
||||
SessionNotFound: 'SESSION_NOT_FOUND',
|
||||
SessionExpired: 'SESSION_EXPIRED',
|
||||
InvalidToken: 'INVALID_TOKEN',
|
||||
MissingToken: 'MISSING_TOKEN',
|
||||
InvalidRequest: 'INVALID_REQUEST',
|
||||
UnverifiedEmail: 'UNVERIFIED_EMAIL',
|
||||
NotFound: 'NOT_FOUND',
|
||||
NotSetup: 'NOT_SETUP',
|
||||
|
||||
// InternalSeverError: 'INTERNAL_SEVER_ERROR',
|
||||
// TwoFactorAlreadyEnabled: 'TWO_FACTOR_ALREADY_ENABLED',
|
||||
// TwoFactorSetupRequired: 'TWO_FACTOR_SETUP_REQUIRED',
|
||||
// TwoFactorMissingSecret: 'TWO_FACTOR_MISSING_SECRET',
|
||||
// TwoFactorMissingCredentials: 'TWO_FACTOR_MISSING_CREDENTIALS',
|
||||
InvalidTwoFactorCode: 'INVALID_TWO_FACTOR_CODE',
|
||||
// IncorrectTwoFactorBackupCode: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
|
||||
// IncorrectIdentityProvider: 'INCORRECT_IDENTITY_PROVIDER',
|
||||
// IncorrectPassword: 'INCORRECT_PASSWORD',
|
||||
// MissingEncryptionKey: 'MISSING_ENCRYPTION_KEY',
|
||||
// MissingBackupCode: 'MISSING_BACKUP_CODE',
|
||||
} as const;
|
||||
|
||||
export type AuthenticationErrorCode =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
(typeof AuthenticationErrorCode)[keyof typeof AuthenticationErrorCode] | (string & {});
|
||||
34
packages/auth/server/lib/session/session-cookies.ts
Normal file
34
packages/auth/server/lib/session/session-cookies.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { Context } from 'hono';
|
||||
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
|
||||
|
||||
import { authDebugger } from '../utils/debugger';
|
||||
|
||||
/**
|
||||
* Get the session cookie attached to the request headers.
|
||||
*
|
||||
* @param c - The Hono context.
|
||||
*/
|
||||
export const getSessionCookie = async (c: Context) => {
|
||||
const sessionId = await getSignedCookie(c, 'secret', 'sessionId');
|
||||
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the session cookie into the Hono context.
|
||||
*
|
||||
* @param c - The Hono context.
|
||||
* @param sessionToken - The session token to set.
|
||||
*/
|
||||
export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
||||
await setSignedCookie(c, 'sessionId', sessionToken, 'secret', {
|
||||
path: '/',
|
||||
// sameSite: '', // whats the default? we need to change this for embed right?
|
||||
// secure: true,
|
||||
domain: 'localhost', // todo
|
||||
}).catch((err) => {
|
||||
authDebugger(`Error setting signed cookie: ${err}`);
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
107
packages/auth/server/lib/session/session.ts
Normal file
107
packages/auth/server/lib/session/session.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import type { Session, User } from '@prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type SessionValidationResult =
|
||||
| {
|
||||
session: Session;
|
||||
user: Pick<
|
||||
User,
|
||||
'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' // Todo
|
||||
>;
|
||||
isAuthenticated: true;
|
||||
}
|
||||
| { session: null; user: null; isAuthenticated: false };
|
||||
|
||||
export const generateSessionToken = (): string => {
|
||||
const bytes = new Uint8Array(20);
|
||||
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
const token = encodeBase32LowerCaseNoPadding(bytes);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export const createSession = async (
|
||||
token: string,
|
||||
userId: number,
|
||||
metadata: RequestMetadata,
|
||||
): Promise<Session> => {
|
||||
const hashedSessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
|
||||
const session: Session = {
|
||||
id: hashedSessionId,
|
||||
sessionToken: hashedSessionId, // todo
|
||||
userId,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
||||
ipAddress: metadata.ipAddress ?? null,
|
||||
userAgent: metadata.userAgent ?? null,
|
||||
};
|
||||
|
||||
await prisma.session.create({
|
||||
data: session,
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
export const validateSessionToken = async (token: string): Promise<SessionValidationResult> => {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
|
||||
const result = await prisma.session.findUnique({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
// user: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// emailVerified: true,
|
||||
// avatarImageId: true,
|
||||
// twoFactorEnabled: true,
|
||||
// },
|
||||
// },
|
||||
|
||||
// todo; how can result.user be null?
|
||||
if (result === null || !result.user) {
|
||||
return { session: null, user: null, isAuthenticated: false };
|
||||
}
|
||||
|
||||
const { user, ...session } = result;
|
||||
|
||||
if (Date.now() >= session.expiresAt.getTime()) {
|
||||
await prisma.session.delete({ where: { id: sessionId } });
|
||||
return { session: null, user: null, isAuthenticated: false };
|
||||
}
|
||||
|
||||
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
|
||||
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
|
||||
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
expiresAt: session.expiresAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { session, user, isAuthenticated: true };
|
||||
};
|
||||
|
||||
export const invalidateSession = async (sessionId: string): Promise<void> => {
|
||||
await prisma.session.delete({ where: { id: sessionId } });
|
||||
};
|
||||
22
packages/auth/server/lib/utils/authorizer.ts
Normal file
22
packages/auth/server/lib/utils/authorizer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Context } from 'hono';
|
||||
|
||||
import type { HonoAuthContext } from '../../types/context';
|
||||
import { createSession, generateSessionToken } from '../session/session';
|
||||
import { setSessionCookie } from '../session/session-cookies';
|
||||
|
||||
type AuthorizeUser = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles creating a session.
|
||||
*/
|
||||
export const onAuthorize = async (user: AuthorizeUser, c: Context<HonoAuthContext>) => {
|
||||
const metadata = c.get('requestMetadata');
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
|
||||
await createSession(sessionToken, user.userId, metadata);
|
||||
|
||||
await setSessionCookie(c, sessionToken);
|
||||
};
|
||||
5
packages/auth/server/lib/utils/debugger.ts
Normal file
5
packages/auth/server/lib/utils/debugger.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const authDebugger = (message: string) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[DEBUG]: ${message}`);
|
||||
}
|
||||
};
|
||||
57
packages/auth/server/lib/utils/get-session.ts
Normal file
57
packages/auth/server/lib/utils/get-session.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type { Context } from 'hono';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import { AuthenticationErrorCode } from '../errors/error-codes';
|
||||
import type { SessionValidationResult } from '../session/session';
|
||||
import { validateSessionToken } from '../session/session';
|
||||
import { getSessionCookie } from '../session/session-cookies';
|
||||
import { authDebugger } from './debugger';
|
||||
|
||||
export const getSession = async (c: Context | Request): Promise<SessionValidationResult> => {
|
||||
// Todo: Make better
|
||||
const sessionId = await getSessionCookie(mapRequestToContextForCookie(c));
|
||||
|
||||
authDebugger(`Session ID: ${sessionId}`);
|
||||
|
||||
if (!sessionId) {
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
session: null,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
return await validateSessionToken(sessionId);
|
||||
};
|
||||
|
||||
export const getRequiredSession = async (c: Context | Request) => {
|
||||
const { session, user } = await getSession(mapRequestToContextForCookie(c));
|
||||
|
||||
if (session && user) {
|
||||
return { session, user };
|
||||
}
|
||||
|
||||
// Todo: Test if throwing errors work
|
||||
if (c instanceof Request) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
throw new AppError(AuthenticationErrorCode.Unauthorized);
|
||||
};
|
||||
|
||||
const mapRequestToContextForCookie = (c: Context | Request) => {
|
||||
if (c instanceof Request) {
|
||||
// c.req.raw.headers.
|
||||
const partialContext = {
|
||||
req: {
|
||||
raw: c,
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return partialContext as unknown as Context;
|
||||
}
|
||||
|
||||
return c;
|
||||
};
|
||||
342
packages/auth/server/routes/email-password.ts
Normal file
342
packages/auth/server/routes/email-password.ts
Normal file
@ -0,0 +1,342 @@
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { compare } from '@node-rs/bcrypt';
|
||||
import { Hono } from 'hono';
|
||||
import { DateTime } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
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';
|
||||
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
|
||||
import { isTwoFactorAuthenticationEnabled } from '@documenso/lib/server-only/2fa/is-2fa-availble';
|
||||
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
||||
import { validateTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/validate-2fa';
|
||||
import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-codes';
|
||||
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 { 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 type { HonoAuthContext } from '../types/context';
|
||||
import {
|
||||
ZForgotPasswordSchema,
|
||||
ZResetPasswordSchema,
|
||||
ZSignInFormSchema,
|
||||
ZSignUpRequestSchema,
|
||||
ZVerifyEmailSchema,
|
||||
} from '../types/email-password';
|
||||
|
||||
export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
/**
|
||||
* Authorize endpoint.
|
||||
*/
|
||||
.post('/authorize', zValidator('json', ZSignInFormSchema), async (c) => {
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
const { email, password, totpCode, backupCode } = c.req.valid('json');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
throw new AppError(AuthenticationErrorCode.NotFound, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isPasswordsSame = await compare(password, user.password);
|
||||
|
||||
if (!isPasswordsSame) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
|
||||
},
|
||||
});
|
||||
|
||||
throw new AppError(AuthenticationErrorCode.InvalidCredentials, {
|
||||
message: 'Invalid email or 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 AppError(AuthenticationErrorCode.IncorrectTwoFactorCode);
|
||||
}
|
||||
}
|
||||
|
||||
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 AppError('UNVERIFIED_EMAIL', {
|
||||
message: 'Unverified email',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
throw new AppError('ACCOUNT_DISABLED', {
|
||||
message: 'Account disabled',
|
||||
});
|
||||
}
|
||||
|
||||
await onAuthorize({ userId: user.id }, c);
|
||||
|
||||
return c.text('', 201);
|
||||
})
|
||||
/**
|
||||
* Signup endpoint.
|
||||
*/
|
||||
.post('/signup', zValidator('json', ZSignUpRequestSchema), async (c) => {
|
||||
// if (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',
|
||||
// });
|
||||
// }
|
||||
|
||||
const user = await createUser({ name, email, password, signature, url });
|
||||
|
||||
await jobsClient.triggerJob({
|
||||
name: 'send.signup.confirmation.email',
|
||||
payload: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Check this.
|
||||
return c.json({
|
||||
user,
|
||||
});
|
||||
})
|
||||
/**
|
||||
* Verify email endpoint.
|
||||
*/
|
||||
.post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
|
||||
await verifyEmail({ token: c.req.valid('json').token });
|
||||
|
||||
return c.text('OK', 201);
|
||||
})
|
||||
/**
|
||||
* Forgot password endpoint.
|
||||
*/
|
||||
.post('/forgot-password', zValidator('json', ZForgotPasswordSchema), async (c) => {
|
||||
const { email } = c.req.valid('json');
|
||||
|
||||
await forgotPassword({
|
||||
email,
|
||||
});
|
||||
|
||||
return c.text('OK', 201);
|
||||
})
|
||||
/**
|
||||
* Reset password endpoint.
|
||||
*/
|
||||
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
|
||||
const { token, password } = c.req.valid('json');
|
||||
|
||||
await resetPassword({
|
||||
token,
|
||||
password,
|
||||
});
|
||||
|
||||
return c.text('OK', 201);
|
||||
})
|
||||
/**
|
||||
* Setup two factor authentication.
|
||||
*/
|
||||
.post('/2fa/setup', async (c) => {
|
||||
const { user } = await getRequiredSession(c);
|
||||
|
||||
const result = await setupTwoFactorAuthentication({
|
||||
user,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
secret: result.secret,
|
||||
uri: result.uri,
|
||||
});
|
||||
})
|
||||
/**
|
||||
* Enable two factor authentication.
|
||||
*/
|
||||
.post(
|
||||
'/2fa/enable',
|
||||
zValidator(
|
||||
'json',
|
||||
z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
const { user: sessionUser } = await getRequiredSession(c);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: sessionUser.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AuthenticationErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
const { code } = c.req.valid('json');
|
||||
|
||||
const result = await enableTwoFactorAuthentication({
|
||||
user,
|
||||
code,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
},
|
||||
)
|
||||
/**
|
||||
* Disable two factor authentication.
|
||||
*/
|
||||
.post(
|
||||
'/2fa/disable',
|
||||
zValidator(
|
||||
'json',
|
||||
z.object({
|
||||
totpCode: z.string().trim().optional(),
|
||||
backupCode: z.string().trim().optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
const { user: sessionUser } = await getRequiredSession(c);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: sessionUser.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AuthenticationErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
const { totpCode, backupCode } = c.req.valid('json');
|
||||
|
||||
await disableTwoFactorAuthentication({
|
||||
user,
|
||||
totpCode,
|
||||
backupCode,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
)
|
||||
/**
|
||||
* View backup codes.
|
||||
*/
|
||||
.post(
|
||||
'/2fa/view-recovery-codes',
|
||||
zValidator(
|
||||
'json',
|
||||
z.object({
|
||||
token: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { user: sessionUser } = await getRequiredSession(c);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: sessionUser.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
twoFactorBackupCodes: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AuthenticationErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
const { token } = c.req.valid('json');
|
||||
|
||||
const backupCodes = await viewBackupCodes({
|
||||
user,
|
||||
token,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
backupCodes,
|
||||
});
|
||||
},
|
||||
);
|
||||
202
packages/auth/server/routes/google.ts
Normal file
202
packages/auth/server/routes/google.ts
Normal file
@ -0,0 +1,202 @@
|
||||
import { Google, decodeIdToken, generateCodeVerifier, generateState } from 'arctic';
|
||||
import { Hono } from 'hono';
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||
import { onAuthorize } from '../lib/utils/authorizer';
|
||||
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,
|
||||
redirectUri: 'http://localhost:3000/api/auth/google/callback',
|
||||
scope: ['openid', 'email', 'profile'],
|
||||
id: 'google',
|
||||
};
|
||||
|
||||
const google = new Google(options.clientId, options.clientSecret, options.redirectUri);
|
||||
|
||||
// todo: NEXT_PRIVATE_OIDC_WELL_KNOWN???
|
||||
|
||||
export const googleRoute = new Hono<HonoAuthContext>()
|
||||
/**
|
||||
* Authorize endpoint.
|
||||
*/
|
||||
.post('/authorize', (c) => {
|
||||
const scopes = options.scope;
|
||||
const state = generateState();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const url = google.createAuthorizationURL(state, codeVerifier, scopes);
|
||||
|
||||
setCookie(c, 'google_oauth_state', state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
setCookie(c, 'google_code_verifier', codeVerifier, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
// return new Response(null, {
|
||||
// status: 302,
|
||||
// headers: {
|
||||
// Location: url.toString()
|
||||
// }
|
||||
// });
|
||||
|
||||
return c.json({
|
||||
redirectUrl: url,
|
||||
});
|
||||
})
|
||||
/**
|
||||
* Google callback verification.
|
||||
*/
|
||||
.get('/callback', async (c) => {
|
||||
// Todo: Use ZValidator to validate query params.
|
||||
|
||||
const code = c.req.query('code');
|
||||
const state = c.req.query('state');
|
||||
|
||||
const storedState = getCookie(c, 'google_oauth_state');
|
||||
const storedCodeVerifier = getCookie(c, 'google_code_verifier');
|
||||
|
||||
if (!code || !storedState || state !== storedState || !storedCodeVerifier) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Invalid or missing state',
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier);
|
||||
const accessToken = tokens.accessToken();
|
||||
const accessTokenExpiresAt = tokens.accessTokenExpiresAt();
|
||||
const idToken = tokens.idToken();
|
||||
|
||||
console.log(tokens);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const claims = decodeIdToken(tokens.idToken()) as Record<string, unknown>;
|
||||
|
||||
console.log(claims);
|
||||
|
||||
const googleEmail = claims.email;
|
||||
const googleName = claims.name;
|
||||
const googleSub = claims.sub;
|
||||
|
||||
if (
|
||||
typeof googleEmail !== 'string' ||
|
||||
typeof googleName !== 'string' ||
|
||||
typeof googleSub !== 'string'
|
||||
) {
|
||||
throw new AppError(AuthenticationErrorCode.InvalidRequest, {
|
||||
message: 'Invalid google claims',
|
||||
});
|
||||
}
|
||||
|
||||
if (claims.email_verified !== true) {
|
||||
throw new AppError(AuthenticationErrorCode.UnverifiedEmail, {
|
||||
message: 'Account email is not verified',
|
||||
});
|
||||
}
|
||||
|
||||
// Find the account if possible.
|
||||
const existingAccount = await prisma.account.findFirst({
|
||||
where: {
|
||||
provider: 'google',
|
||||
providerAccountId: googleSub,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Directly log in user if account already exists.
|
||||
if (existingAccount) {
|
||||
await onAuthorize({ userId: existingAccount.user.id }, c);
|
||||
|
||||
return c.redirect('/documents', 302); // Todo: Redirect
|
||||
}
|
||||
|
||||
const userWithSameEmail = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: googleEmail,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle existing user but no account.
|
||||
if (userWithSameEmail) {
|
||||
await prisma.account.create({
|
||||
data: {
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: googleSub,
|
||||
access_token: accessToken,
|
||||
expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000),
|
||||
token_type: 'Bearer',
|
||||
id_token: idToken,
|
||||
userId: userWithSameEmail.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Link account
|
||||
await onAuthorize({ userId: userWithSameEmail.id }, c);
|
||||
|
||||
return c.redirect('/documents', 302); // Todo: Redirect
|
||||
}
|
||||
|
||||
// Handle new user.
|
||||
const createdUser = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({
|
||||
data: {
|
||||
email: googleEmail,
|
||||
name: googleName,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.account.create({
|
||||
data: {
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: googleSub,
|
||||
access_token: accessToken,
|
||||
expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000),
|
||||
token_type: 'Bearer',
|
||||
id_token: idToken,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
await onAuthorize({ userId: createdUser.id }, c);
|
||||
|
||||
return c.redirect('/documents', 302); // Todo: Redirect
|
||||
})
|
||||
/**
|
||||
* Setup passkey authentication.
|
||||
*/
|
||||
.post('/setup', async (c) => {
|
||||
const { user } = await getRequiredSession(c);
|
||||
|
||||
const result = await setupTwoFactorAuthentication({
|
||||
user,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
secret: result.secret,
|
||||
uri: result.uri,
|
||||
});
|
||||
});
|
||||
144
packages/auth/server/routes/passkey.ts
Normal file
144
packages/auth/server/routes/passkey.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { onAuthorize } from '../lib/utils/authorizer';
|
||||
import { getRequiredSession } from '../lib/utils/get-session';
|
||||
import type { HonoAuthContext } from '../types/context';
|
||||
import { ZPasskeyAuthorizeSchema } from '../types/passkey';
|
||||
|
||||
export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
.post('/authorize', zValidator('json', ZPasskeyAuthorizeSchema), async (c) => {
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
const { csrfToken, credential } = c.req.valid('json');
|
||||
|
||||
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
|
||||
|
||||
try {
|
||||
const parsedBodyCredential = JSON.parse(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);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
await onAuthorize({ userId: user.id }, c);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
url: '/documents',
|
||||
},
|
||||
200,
|
||||
);
|
||||
})
|
||||
|
||||
.post('/register', async (c) => {
|
||||
const { user } = await getRequiredSession(c);
|
||||
|
||||
//
|
||||
})
|
||||
|
||||
.post(
|
||||
'/pre-authenticate',
|
||||
zValidator(
|
||||
'json',
|
||||
z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
//
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
},
|
||||
);
|
||||
10
packages/auth/server/routes/session.ts
Normal file
10
packages/auth/server/routes/session.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import type { SessionValidationResult } from '../lib/session/session';
|
||||
import { getSession } from '../lib/utils/get-session';
|
||||
|
||||
export const sessionRoute = new Hono().get('/session', async (c) => {
|
||||
const session: SessionValidationResult = await getSession(c);
|
||||
|
||||
return c.json(session);
|
||||
});
|
||||
29
packages/auth/server/routes/sign-out.ts
Normal file
29
packages/auth/server/routes/sign-out.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Hono } from 'hono';
|
||||
import { deleteCookie, getSignedCookie } from 'hono/cookie';
|
||||
|
||||
import { invalidateSession, validateSessionToken } from '../lib/session/session';
|
||||
|
||||
export const signOutRoute = new Hono().post('/signout', async (c) => {
|
||||
// todo: secret
|
||||
const sessionId = await getSignedCookie(c, 'secret', 'sessionId');
|
||||
|
||||
if (!sessionId) {
|
||||
return new Response('No session found', { status: 401 });
|
||||
}
|
||||
|
||||
const { session } = await validateSessionToken(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return new Response('No session found', { status: 401 });
|
||||
}
|
||||
|
||||
await invalidateSession(session.id);
|
||||
|
||||
deleteCookie(c, 'sessionId', {
|
||||
path: '/',
|
||||
secure: true,
|
||||
domain: 'example.com',
|
||||
});
|
||||
|
||||
return c.status(200);
|
||||
});
|
||||
7
packages/auth/server/types/context.ts
Normal file
7
packages/auth/server/types/context.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
|
||||
export type HonoAuthContext = {
|
||||
Variables: {
|
||||
requestMetadata: RequestMetadata;
|
||||
};
|
||||
};
|
||||
69
packages/auth/server/types/email-password.ts
Normal file
69
packages/auth/server/types/email-password.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCurrentPasswordSchema = z
|
||||
.string()
|
||||
.min(6, { message: 'Must be at least 6 characters in length' })
|
||||
.max(72);
|
||||
|
||||
export const ZSignInFormSchema = 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 const ZPasswordSchema = z
|
||||
.string()
|
||||
.min(8, { message: 'Must be at least 8 characters in length' })
|
||||
.max(72, { message: 'Cannot be more than 72 characters in length' })
|
||||
.refine((value) => value.length > 25 || /[A-Z]/.test(value), {
|
||||
message: 'One uppercase character',
|
||||
})
|
||||
.refine((value) => value.length > 25 || /[a-z]/.test(value), {
|
||||
message: 'One lowercase character',
|
||||
})
|
||||
.refine((value) => value.length > 25 || /\d/.test(value), {
|
||||
message: 'One number',
|
||||
})
|
||||
.refine((value) => value.length > 25 || /[`~<>?,./!@#$%^&*()\-_"'+=|{}[\];:\\]/.test(value), {
|
||||
message: 'One special character is required',
|
||||
});
|
||||
|
||||
export const ZSignUpRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: ZPasswordSchema,
|
||||
signature: z.string().nullish(),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(1)
|
||||
.regex(/^[a-z0-9-]+$/, {
|
||||
message: 'Username can only container alphanumeric characters and dashes.',
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TSignUpRequestSchema = z.infer<typeof ZSignUpRequestSchema>;
|
||||
|
||||
export const ZForgotPasswordSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
});
|
||||
|
||||
export type TForgotPasswordSchema = z.infer<typeof ZForgotPasswordSchema>;
|
||||
|
||||
export const ZResetPasswordSchema = z.object({
|
||||
password: ZPasswordSchema,
|
||||
token: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TResetPasswordSchema = z.infer<typeof ZResetPasswordSchema>;
|
||||
|
||||
export const ZVerifyEmailSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
|
||||
8
packages/auth/server/types/passkey.ts
Normal file
8
packages/auth/server/types/passkey.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZPasskeyAuthorizeSchema = z.object({
|
||||
csrfToken: z.string().min(1),
|
||||
credential: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TPasskeyAuthorizeSchema = z.infer<typeof ZPasskeyAuthorizeSchema>;
|
||||
8
packages/auth/tsconfig.json
Normal file
8
packages/auth/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
|
||||
import { ERROR_CODES } from './errors';
|
||||
import type { TLimitsErrorResponseSchema, TLimitsResponseSchema } from './schema';
|
||||
import { getServerLimits } from './server';
|
||||
|
||||
export const limitsHandler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<TLimitsResponseSchema | TLimitsErrorResponseSchema>,
|
||||
) => {
|
||||
try {
|
||||
const token = await getToken({ req });
|
||||
// res: NextApiResponse<TLimitsResponseSchema | TLimitsErrorResponseSchema>,
|
||||
|
||||
const rawTeamId = req.headers['team-id'];
|
||||
export const limitsHandler = async (req: Request) => {
|
||||
try {
|
||||
// Todo: Check
|
||||
const { user } = await getSession(req);
|
||||
|
||||
const rawTeamId = req.headers.get('team-id');
|
||||
|
||||
let teamId: number | null = null;
|
||||
|
||||
@ -26,9 +24,11 @@ export const limitsHandler = async (
|
||||
throw new Error(ERROR_CODES.INVALID_TEAM_ID);
|
||||
}
|
||||
|
||||
const limits = await getServerLimits({ email: token?.email, teamId });
|
||||
const limits = await getServerLimits({ email: user?.email, teamId });
|
||||
|
||||
return res.status(200).json(limits);
|
||||
return Response.json(limits, {
|
||||
status: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('error', err);
|
||||
|
||||
@ -37,13 +37,23 @@ export const limitsHandler = async (
|
||||
.with(ERROR_CODES.UNAUTHORIZED, () => 401)
|
||||
.otherwise(() => 500);
|
||||
|
||||
return res.status(status).json({
|
||||
error: ERROR_CODES[err.message] ?? ERROR_CODES.UNKNOWN,
|
||||
});
|
||||
return Response.json(
|
||||
{
|
||||
error: ERROR_CODES[err.message] ?? ERROR_CODES.UNKNOWN,
|
||||
},
|
||||
{
|
||||
status,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: ERROR_CODES.UNKNOWN,
|
||||
});
|
||||
return Response.json(
|
||||
{
|
||||
error: ERROR_CODES.UNKNOWN,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
@ -89,7 +89,7 @@ const getTransport = (): Transporter => {
|
||||
}
|
||||
|
||||
return createTransport({
|
||||
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? 'localhost:2500',
|
||||
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',
|
||||
|
||||
@ -45,4 +45,4 @@
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"tsup": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import * as ReactEmail from '@react-email/render';
|
||||
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import { Tailwind } from './components';
|
||||
import { BrandingProvider, type BrandingSettings } from './providers/branding';
|
||||
|
||||
// Todo:
|
||||
// import config from '@documenso/tailwind-config';
|
||||
|
||||
export type RenderOptions = ReactEmail.Options & {
|
||||
branding?: BrandingSettings;
|
||||
};
|
||||
@ -17,7 +18,7 @@ export const render = (element: React.ReactNode, options?: RenderOptions) => {
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: config.theme.extend.colors,
|
||||
// colors: config.theme.extend.colors,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@ -36,7 +37,7 @@ export const renderAsync = async (element: React.ReactNode, options?: RenderOpti
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: config.theme.extend.colors,
|
||||
// colors: config.theme.extend.colors,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@ -2,12 +2,12 @@ import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
import { Body, Button, Container, Head, Html, Img, Preview, Section, Text } from '../components';
|
||||
import { useBranding } from '../providers/branding';
|
||||
import TemplateDocumentImage from '../template-components/template-document-image';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import { RecipientRole } from '.prisma/client';
|
||||
|
||||
export type DocumentCompletedEmailTemplateProps = {
|
||||
recipientName?: string;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
|
||||
import { getFile } from '../universal/upload/get-file';
|
||||
import { downloadFile } from './download-file';
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { Field } from '@prisma/client';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
export const useFieldPageCoords = (field: Field) => {
|
||||
const [coords, setCoords] = useState({
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router';
|
||||
|
||||
export const useUpdateSearchParams = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (params: Record<string, string | number | boolean | null | undefined>) => {
|
||||
const nextSearchParams = new URLSearchParams(searchParams?.toString() ?? '');
|
||||
@ -16,6 +16,6 @@ export const useUpdateSearchParams = () => {
|
||||
}
|
||||
});
|
||||
|
||||
router.push(`${pathname}?${nextSearchParams.toString()}`);
|
||||
void navigate(`${pathname}?${nextSearchParams.toString()}`);
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
import 'server-only';
|
||||
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import type { I18n, Messages } from '@lingui/core';
|
||||
import { setupI18n } from '@lingui/core';
|
||||
import { setI18n } from '@lingui/react/server';
|
||||
|
||||
import {
|
||||
APP_I18N_OPTIONS,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
isValidLanguageCode,
|
||||
} from '../../constants/i18n';
|
||||
import { extractLocaleData } from '../../utils/i18n';
|
||||
import { remember } from '../../utils/remember';
|
||||
|
||||
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||
@ -21,7 +15,8 @@ export async function loadCatalog(lang: SupportedLanguages): Promise<{
|
||||
}> {
|
||||
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
|
||||
|
||||
const { messages } = await import(`../../translations/${lang}/web.${extension}`);
|
||||
// const { messages } = await import(`../../translations/${lang}/web.${extension}`);
|
||||
const messages = {};
|
||||
|
||||
return {
|
||||
[lang]: messages,
|
||||
@ -71,29 +66,3 @@ export const getI18nInstance = async (lang?: SupportedLanguages | (string & {}))
|
||||
|
||||
return instances[lang] ?? instances[APP_I18N_OPTIONS.sourceLang];
|
||||
};
|
||||
|
||||
/**
|
||||
* This needs to be run in all layouts and page server components that require i18n.
|
||||
*
|
||||
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
|
||||
*/
|
||||
export const setupI18nSSR = async () => {
|
||||
const { lang, locales } = extractLocaleData({
|
||||
cookies: cookies(),
|
||||
headers: headers(),
|
||||
});
|
||||
|
||||
// Get and set a ready-made i18n instance for the given language.
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
// Reactivate the i18n instance with the locale for date and number formatting.
|
||||
i18n.activate(lang, locales);
|
||||
|
||||
setI18n(i18n);
|
||||
|
||||
return {
|
||||
lang,
|
||||
locales,
|
||||
i18n,
|
||||
};
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
} from '@prisma/client';
|
||||
|
||||
export enum RecipientStatusType {
|
||||
COMPLETED = 'completed',
|
||||
|
||||
@ -3,7 +3,8 @@ import { env } from 'next-runtime-env';
|
||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
||||
|
||||
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
||||
// 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();
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
// Todo: Reimport
|
||||
|
||||
export const SALT_ROUNDS = 12;
|
||||
|
||||
export const IDENTITY_PROVIDER_NAME: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.DOCUMENSO]: 'Documenso',
|
||||
[IdentityProvider.GOOGLE]: 'Google',
|
||||
[IdentityProvider.OIDC]: 'OIDC',
|
||||
export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
|
||||
DOCUMENSO: 'Documenso',
|
||||
GOOGLE: 'Google',
|
||||
OIDC: 'OIDC',
|
||||
};
|
||||
|
||||
export const IS_GOOGLE_SSO_ENABLED = Boolean(
|
||||
process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
|
||||
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID &&
|
||||
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
|
||||
);
|
||||
|
||||
export const IS_OIDC_SSO_ENABLED = Boolean(
|
||||
@ -20,21 +21,21 @@ export const IS_OIDC_SSO_ENABLED = Boolean(
|
||||
|
||||
export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL;
|
||||
|
||||
export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: string } = {
|
||||
[UserSecurityAuditLogType.ACCOUNT_SSO_LINK]: 'Linked account to SSO',
|
||||
[UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE]: 'Profile updated',
|
||||
[UserSecurityAuditLogType.AUTH_2FA_DISABLE]: '2FA Disabled',
|
||||
[UserSecurityAuditLogType.AUTH_2FA_ENABLE]: '2FA Enabled',
|
||||
[UserSecurityAuditLogType.PASSKEY_CREATED]: 'Passkey created',
|
||||
[UserSecurityAuditLogType.PASSKEY_DELETED]: 'Passkey deleted',
|
||||
[UserSecurityAuditLogType.PASSKEY_UPDATED]: 'Passkey updated',
|
||||
[UserSecurityAuditLogType.PASSWORD_RESET]: 'Password reset',
|
||||
[UserSecurityAuditLogType.PASSWORD_UPDATE]: 'Password updated',
|
||||
[UserSecurityAuditLogType.SIGN_OUT]: 'Signed Out',
|
||||
[UserSecurityAuditLogType.SIGN_IN]: 'Signed In',
|
||||
[UserSecurityAuditLogType.SIGN_IN_FAIL]: 'Sign in attempt failed',
|
||||
[UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL]: 'Passkey sign in failed',
|
||||
[UserSecurityAuditLogType.SIGN_IN_2FA_FAIL]: 'Sign in 2FA attempt failed',
|
||||
export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
|
||||
ACCOUNT_SSO_LINK: 'Linked account to SSO',
|
||||
ACCOUNT_PROFILE_UPDATE: 'Profile updated',
|
||||
AUTH_2FA_DISABLE: '2FA Disabled',
|
||||
AUTH_2FA_ENABLE: '2FA Enabled',
|
||||
PASSKEY_CREATED: 'Passkey created',
|
||||
PASSKEY_DELETED: 'Passkey deleted',
|
||||
PASSKEY_UPDATED: 'Passkey updated',
|
||||
PASSWORD_RESET: 'Password reset',
|
||||
PASSWORD_UPDATE: 'Password updated',
|
||||
SIGN_OUT: 'Signed Out',
|
||||
SIGN_IN: 'Signed In',
|
||||
SIGN_IN_FAIL: 'Sign in attempt failed',
|
||||
SIGN_IN_PASSKEY_FAIL: 'Passkey sign in failed',
|
||||
SIGN_IN_2FA_FAIL: 'Sign in 2FA attempt failed',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const DOCUMENSO_ENCRYPTION_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_KEY;
|
||||
export const DOCUMENSO_ENCRYPTION_KEY = '12345678912345678912345678912457';
|
||||
|
||||
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY;
|
||||
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = '12345678912345678912345678912458';
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||
|
||||
export const DOCUMENT_STATUS: {
|
||||
[status in DocumentStatus]: { description: MessageDescriptor };
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
|
||||
export const RECIPIENT_ROLES_DESCRIPTION = {
|
||||
[RecipientRole.APPROVER]: {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+$');
|
||||
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||
@ -19,6 +20,7 @@ export const jobsClient = new JobClient([
|
||||
SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
|
||||
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
|
||||
|
||||
export abstract class BaseJobProvider {
|
||||
@ -16,4 +18,8 @@ export abstract class BaseJobProvider {
|
||||
public getApiHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise<Response | void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public getHonoApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,4 +27,8 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
||||
public getApiHandler() {
|
||||
return this._provider.getApiHandler();
|
||||
}
|
||||
|
||||
public getHonoApiHandler() {
|
||||
return this._provider.getHonoApiHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
import type { Context, Handler, InngestFunction } from 'inngest';
|
||||
import { Inngest as InngestClient } from 'inngest';
|
||||
import { serve as createHonoPagesRoute } from 'inngest/hono';
|
||||
import type { Logger } from 'inngest/middleware/logger';
|
||||
import { serve as createPagesRoute } from 'inngest/next';
|
||||
import { json } from 'micro';
|
||||
@ -94,6 +96,18 @@ export class InngestJobProvider extends BaseJobProvider {
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: Do we need to handle the above?
|
||||
public getHonoApiHandler() {
|
||||
return async (context: HonoContext) => {
|
||||
const handler = createHonoPagesRoute({
|
||||
client: this._client,
|
||||
functions: this._functions,
|
||||
});
|
||||
|
||||
return await handler(context);
|
||||
};
|
||||
}
|
||||
|
||||
private convertInngestIoToJobRunIo(ctx: Context.Any & { logger: Logger }) {
|
||||
const { step } = ctx;
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { BackgroundJobStatus, Prisma } from '@prisma/client';
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
import { json } from 'micro';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
import { sign } from '../../server-only/crypto/sign';
|
||||
@ -213,6 +214,152 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
};
|
||||
}
|
||||
|
||||
public getHonoApiHandler(): (context: HonoContext) => Promise<Response | void> {
|
||||
return async (context: HonoContext) => {
|
||||
const req = context.req;
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
context.text('Method not allowed', 405);
|
||||
return;
|
||||
}
|
||||
|
||||
const jobId = req.header('x-job-id');
|
||||
const signature = req.header('x-job-signature');
|
||||
const isRetry = req.header('x-job-retry') !== undefined;
|
||||
|
||||
const options = await req
|
||||
.json()
|
||||
.then(async (data) => ZSimpleTriggerJobOptionsSchema.parseAsync(data))
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
.then((data) => data as SimpleTriggerJobOptions)
|
||||
.catch(() => null);
|
||||
|
||||
if (!options) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = this._jobDefinitions[options.name];
|
||||
|
||||
if (
|
||||
typeof jobId !== 'string' ||
|
||||
typeof signature !== 'string' ||
|
||||
typeof options !== 'object'
|
||||
) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition && !definition.enabled) {
|
||||
console.log('Attempted to trigger a disabled job', options.name);
|
||||
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signature || !verify(options, signature)) {
|
||||
context.text('Unauthorized', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.trigger.schema) {
|
||||
const result = definition.trigger.schema.safeParse(options.payload);
|
||||
|
||||
if (!result.success) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[JOBS]: Triggering job ${options.name} with payload`, options.payload);
|
||||
|
||||
let backgroundJob = await prisma.backgroundJob
|
||||
.update({
|
||||
where: {
|
||||
id: jobId,
|
||||
status: BackgroundJobStatus.PENDING,
|
||||
},
|
||||
data: {
|
||||
status: BackgroundJobStatus.PROCESSING,
|
||||
retried: {
|
||||
increment: isRetry ? 1 : 0,
|
||||
},
|
||||
lastRetriedAt: isRetry ? new Date() : undefined,
|
||||
},
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!backgroundJob) {
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await definition.handler({
|
||||
payload: options.payload,
|
||||
io: this.createJobRunIO(jobId),
|
||||
});
|
||||
|
||||
backgroundJob = await prisma.backgroundJob.update({
|
||||
where: {
|
||||
id: jobId,
|
||||
status: BackgroundJobStatus.PROCESSING,
|
||||
},
|
||||
data: {
|
||||
status: BackgroundJobStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`[JOBS]: Job ${options.name} failed`, error);
|
||||
|
||||
const taskHasExceededRetries = error instanceof BackgroundTaskExceededRetriesError;
|
||||
const jobHasExceededRetries =
|
||||
backgroundJob.retried >= backgroundJob.maxRetries &&
|
||||
!(error instanceof BackgroundTaskFailedError);
|
||||
|
||||
if (taskHasExceededRetries || jobHasExceededRetries) {
|
||||
backgroundJob = await prisma.backgroundJob.update({
|
||||
where: {
|
||||
id: jobId,
|
||||
status: BackgroundJobStatus.PROCESSING,
|
||||
},
|
||||
data: {
|
||||
status: BackgroundJobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
context.text('Task exceeded retries', 500);
|
||||
return;
|
||||
}
|
||||
|
||||
backgroundJob = await prisma.backgroundJob.update({
|
||||
where: {
|
||||
id: jobId,
|
||||
status: BackgroundJobStatus.PROCESSING,
|
||||
},
|
||||
data: {
|
||||
status: BackgroundJobStatus.PENDING,
|
||||
},
|
||||
});
|
||||
|
||||
await this.submitJobToEndpoint({
|
||||
jobId,
|
||||
jobDefinitionId: backgroundJob.jobId,
|
||||
data: options,
|
||||
});
|
||||
}
|
||||
|
||||
context.text('OK', 200);
|
||||
};
|
||||
}
|
||||
|
||||
private async submitJobToEndpoint(options: {
|
||||
jobId: string;
|
||||
jobDefinitionId: string;
|
||||
|
||||
@ -58,6 +58,11 @@ export class TriggerJobProvider extends BaseJobProvider {
|
||||
return handler;
|
||||
}
|
||||
|
||||
// Hono v2 is being deprecated so not sure if we will be required.
|
||||
// public getHonoApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
||||
// throw new Error('Not implemented');
|
||||
// }
|
||||
|
||||
private convertTriggerIoToJobRunIo(io: IO) {
|
||||
return {
|
||||
wait: io.wait,
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { sendResetPassword } from '../../../server-only/auth/send-reset-password';
|
||||
import type { TSendPasswordResetSuccessEmailJobDefinition } from './send-password-reset-success-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendPasswordResetSuccessEmailJobDefinition;
|
||||
}) => {
|
||||
await sendResetPassword({
|
||||
userId: payload.userId,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID = 'send.password.reset.success.email';
|
||||
|
||||
const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendPasswordResetSuccessEmailJobDefinition = z.infer<
|
||||
typeof SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Password Reset Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload }) => {
|
||||
const handler = await import('./send-password-reset-success-email.handler');
|
||||
|
||||
await handler.run({ payload });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendPasswordResetSuccessEmailJobDefinition
|
||||
>;
|
||||
@ -0,0 +1,117 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendRecipientSignedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigned;
|
||||
|
||||
if (!isRecipientSignedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { user: owner } = document;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
// Don't send notification if the owner is the one who signed
|
||||
if (owner.email === recipientEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -1,18 +1,5 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID = 'send.recipient.signed.email';
|
||||
@ -22,6 +9,10 @@ const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendRecipientSignedEmailJobDefinition = z.infer<
|
||||
typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Recipient Signed Email',
|
||||
@ -31,98 +22,9 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
schema: SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
const handler = await import('./send-recipient-signed-email.handler');
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigned;
|
||||
|
||||
if (!isRecipientSignedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { user: owner } = document;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
// Don't send notification if the owner is the one who signed
|
||||
if (owner.email === recipientEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { DocumentSource, DocumentStatus, RecipientRole, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
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';
|
||||
@ -12,7 +13,6 @@ import GoogleProvider from 'next-auth/providers/google';
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Role, User } from '@documenso/prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
export const isAdmin = (user: User) => user.roles.includes(Role.ADMIN);
|
||||
export const isAdmin = (user: Pick<User, 'roles'>) => user.roles.includes(Role.ADMIN);
|
||||
|
||||
@ -63,4 +63,4 @@
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/pg": "^8.11.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import type { User } from '@prisma/client';
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { validateTwoFactorAuthentication } from './validate-2fa';
|
||||
|
||||
type DisableTwoFactorAuthenticationOptions = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
|
||||
totpCode?: string;
|
||||
backupCode?: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { type User, UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { type User, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
@ -7,7 +8,7 @@ import { getBackupCodes } from './get-backup-code';
|
||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||
|
||||
type EnableTwoFactorAuthenticationOptions = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
|
||||
code: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import type { User } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { User } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricDecrypt } from '../../universal/crypto';
|
||||
|
||||
interface GetBackupCodesOptions {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
||||
}
|
||||
|
||||
const ZBackupCodeSchema = z.array(z.string());
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { type User } from '@prisma/client';
|
||||
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 { type User } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricEncrypt } from '../../universal/crypto';
|
||||
|
||||
type SetupTwoFactorAuthenticationOptions = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email'>;
|
||||
};
|
||||
|
||||
const ISSUER = 'Documenso';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { ErrorCode } from '../../next-auth/error-codes';
|
||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||
@ -7,7 +7,7 @@ import { verifyBackupCode } from './verify-backup-code';
|
||||
type ValidateTwoFactorAuthenticationOptions = {
|
||||
totpCode?: string;
|
||||
backupCode?: string;
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret'>;
|
||||
};
|
||||
|
||||
export const validateTwoFactorAuthentication = async ({
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import type { User } from '@prisma/client';
|
||||
import { base32 } from '@scure/base';
|
||||
import { generateHOTP } from 'oslo/otp';
|
||||
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricDecrypt } from '../../universal/crypto';
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { getBackupCodes } from './get-backup-code';
|
||||
|
||||
type VerifyBackupCodeParams = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorBackupCodes'>;
|
||||
backupCode: string;
|
||||
};
|
||||
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { getBackupCodes } from './get-backup-code';
|
||||
import { validateTwoFactorAuthentication } from './validate-2fa';
|
||||
|
||||
type ViewBackupCodesOptions = {
|
||||
user: User;
|
||||
user: Pick<
|
||||
User,
|
||||
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
|
||||
>;
|
||||
token: string;
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export const getRecipientsStats = async () => {
|
||||
const results = await prisma.recipient.groupBy({
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { DocumentStatus, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
export type SigningVolume = {
|
||||
id: number;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
|
||||
|
||||
export const getUsersCount = async () => {
|
||||
return await prisma.user.count();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
id: number;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Role } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Role } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateUserOptions = {
|
||||
id: number;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Passkey } from '@prisma/client';
|
||||
import { generateAuthenticationOptions } from '@simplewebauthn/server';
|
||||
import type { AuthenticatorTransportFuture } from '@simplewebauthn/types';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Passkey } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getAuthenticatorOptions } from '../../utils/authenticator';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
import { verifyRegistrationResponse } from '@simplewebauthn/server';
|
||||
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { MAXIMUM_PASSKEYS } from '../../constants/auth';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Passkey } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Passkey } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import type { DocumentDataType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentDataType } from '@documenso/prisma/client';
|
||||
|
||||
export type CreateDocumentDataOptions = {
|
||||
type: DocumentDataType;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
@ -7,7 +9,6 @@ import {
|
||||
diffDocumentMetaChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
@ -10,7 +5,13 @@ import {
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
} from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
@ -6,15 +16,6 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility, TemplateMeta } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
'use server';
|
||||
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
|
||||
@ -3,10 +3,6 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
@ -14,10 +10,14 @@ import type {
|
||||
Team,
|
||||
TeamGlobalSettings,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
} from '@prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { DocumentSource, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, type Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { DocumentAuditLog, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentAuditLog, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
import type { Document, DocumentSource, Prisma, Team, TeamEmail, User } from '@prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type {
|
||||
Document,
|
||||
DocumentSource,
|
||||
Prisma,
|
||||
Team,
|
||||
TeamEmail,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { Prisma, User } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { Prisma, User } from '@documenso/prisma/client';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||
|
||||
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
@ -14,10 +16,8 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
@ -6,12 +7,6 @@ import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-po
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import type { Document, Recipient, User } from '@prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type SearchDocumentsWithKeywordOptions = {
|
||||
query: string;
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { DocumentSource } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
|
||||
@ -6,7 +6,7 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
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';
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
@ -10,7 +5,13 @@ import {
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
} from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
|
||||
@ -6,7 +6,7 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { DocumentStatus, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
@ -6,8 +8,6 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import type { Document, Field, Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user