fix: use instance-specific emails for service accounts (#2502)

This commit is contained in:
Lucas Smith
2026-02-16 11:52:19 +11:00
committed by GitHub
parent d66c330d46
commit 2e3d22c856
8 changed files with 143 additions and 3 deletions
+5
View File
@@ -12,6 +12,8 @@ import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app';
import { jobsClient } from '@documenso/lib/jobs/client';
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-client';
import { migrateDeletedAccountServiceAccount } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { migrateLegacyServiceAccount } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { env } from '@documenso/lib/utils/env';
import { logger } from '@documenso/lib/utils/logger';
@@ -144,4 +146,7 @@ if (env('NODE_ENV') !== 'development') {
// Start license client to verify license on startup.
void LicenseClient.start();
void migrateDeletedAccountServiceAccount();
void migrateLegacyServiceAccount();
export default app;
@@ -5,6 +5,8 @@ import { deleteCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma';
@@ -26,6 +28,13 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
await validateOauth({ c, clientOptions });
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
// Find the account if possible.
const existingAccount = await prisma.account.findFirst({
where: {
@@ -17,7 +17,10 @@ import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-code
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getMostRecentEmailVerificationToken } from '@documenso/lib/server-only/user/get-most-recent-email-verification-token';
import { getUserByResetToken } from '@documenso/lib/server-only/user/get-user-by-reset-token';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
@@ -57,6 +60,13 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
});
}
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const user = await prisma.user.findFirst({
where: {
email: email.toLowerCase(),
@@ -241,6 +251,13 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
const { email } = c.req.valid('json');
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
await forgotPassword({
email,
});
@@ -253,6 +270,15 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json');
const user = await getUserByResetToken({ token });
if (
user.email.toLowerCase() === legacyServiceAccountEmail() ||
user.email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const requestMetadata = c.get('requestMetadata');
const { userId } = await resetPassword({
+9
View File
@@ -5,6 +5,8 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { Hono } from 'hono';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
@@ -74,6 +76,13 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
const user = passkey.user;
if (
user.email.toLowerCase() === legacyServiceAccountEmail() ||
user.email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const { rpId, origin } = getAuthenticatorOptions();
const verification = await verifyAuthenticationResponse({
-2
View File
@@ -8,8 +8,6 @@ export const DOCUMENSO_INTERNAL_EMAIL = {
address: FROM_ADDRESS,
};
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
export const EMAIL_VERIFICATION_STATE = {
NOT_FOUND: 'NOT_FOUND',
VERIFIED: 'VERIFIED',
@@ -0,0 +1,24 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface GetUserByResetTokenOptions {
token: string;
}
export const getUserByResetToken = async ({ token }: GetUserByResetTokenOptions) => {
const result = await prisma.passwordResetToken.findFirst({
where: {
token,
},
include: {
user: true,
},
});
if (!result || !result.user) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
return result.user;
};
@@ -1,9 +1,27 @@
import { prisma } from '@documenso/prisma';
const LEGACY_DELETED_ACCOUNT_EMAIL = 'deleted-account@documenso.com';
export const deletedServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `deleted-account@${hostname}`;
} catch (error) {
return LEGACY_DELETED_ACCOUNT_EMAIL;
}
};
export const deletedAccountServiceAccount = async () => {
const serviceAccount = await prisma.user.findFirst({
where: {
email: 'deleted-account@documenso.com',
email: deletedServiceAccountEmail(),
},
select: {
id: true,
@@ -29,3 +47,20 @@ export const deletedAccountServiceAccount = async () => {
return serviceAccount;
};
export const migrateDeletedAccountServiceAccount = async () => {
if (deletedServiceAccountEmail() !== LEGACY_DELETED_ACCOUNT_EMAIL) {
console.log(
`Migrating deleted account service account to new email: ${deletedServiceAccountEmail()}`,
);
await prisma.user.updateMany({
where: {
email: LEGACY_DELETED_ACCOUNT_EMAIL,
},
data: {
email: deletedServiceAccountEmail(),
},
});
}
};
@@ -0,0 +1,34 @@
import { prisma } from '@documenso/prisma';
const LEGACY_SERVICE_ACCOUNT_EMAIL = 'serviceaccount@documenso.com';
export const legacyServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `serviceaccount@${hostname}`;
} catch (error) {
return LEGACY_SERVICE_ACCOUNT_EMAIL;
}
};
export const migrateLegacyServiceAccount = async () => {
if (legacyServiceAccountEmail() !== LEGACY_SERVICE_ACCOUNT_EMAIL) {
console.log(`Migrating legacy service account to new email: ${legacyServiceAccountEmail()}`);
await prisma.user.updateMany({
where: {
email: LEGACY_SERVICE_ACCOUNT_EMAIL,
},
data: {
email: legacyServiceAccountEmail(),
},
});
}
};