mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
fix: use instance-specific emails for service accounts (#2502)
This commit is contained in:
@@ -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 { jobsClient } from '@documenso/lib/jobs/client';
|
||||||
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
|
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
|
||||||
import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-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 { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { logger } from '@documenso/lib/utils/logger';
|
import { logger } from '@documenso/lib/utils/logger';
|
||||||
@@ -144,4 +146,7 @@ if (env('NODE_ENV') !== 'development') {
|
|||||||
// Start license client to verify license on startup.
|
// Start license client to verify license on startup.
|
||||||
void LicenseClient.start();
|
void LicenseClient.start();
|
||||||
|
|
||||||
|
void migrateDeletedAccountServiceAccount();
|
||||||
|
void migrateLegacyServiceAccount();
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { deleteCookie } from 'hono/cookie';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
|
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 { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@@ -26,6 +28,13 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
|||||||
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
|
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
|
||||||
await validateOauth({ c, clientOptions });
|
await validateOauth({ c, clientOptions });
|
||||||
|
|
||||||
|
if (
|
||||||
|
email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||||
|
email.toLowerCase() === deletedServiceAccountEmail()
|
||||||
|
) {
|
||||||
|
return c.text('FORBIDDEN', 403);
|
||||||
|
}
|
||||||
|
|
||||||
// Find the account if possible.
|
// Find the account if possible.
|
||||||
const existingAccount = await prisma.account.findFirst({
|
const existingAccount = await prisma.account.findFirst({
|
||||||
where: {
|
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 { createUser } from '@documenso/lib/server-only/user/create-user';
|
||||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
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 { 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 { 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 { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||||
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
|
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
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({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
@@ -241,6 +251,13 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
.post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
|
.post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
|
||||||
const { email } = c.req.valid('json');
|
const { email } = c.req.valid('json');
|
||||||
|
|
||||||
|
if (
|
||||||
|
email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||||
|
email.toLowerCase() === deletedServiceAccountEmail()
|
||||||
|
) {
|
||||||
|
return c.text('FORBIDDEN', 403);
|
||||||
|
}
|
||||||
|
|
||||||
await forgotPassword({
|
await forgotPassword({
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
@@ -253,6 +270,15 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
.post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
|
.post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
|
||||||
const { token, password } = c.req.valid('json');
|
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 requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
const { userId } = await resetPassword({
|
const { userId } = await resetPassword({
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
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 type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||||
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||||
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||||
@@ -74,6 +76,13 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
|||||||
|
|
||||||
const user = passkey.user;
|
const user = passkey.user;
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||||
|
user.email.toLowerCase() === deletedServiceAccountEmail()
|
||||||
|
) {
|
||||||
|
return c.text('FORBIDDEN', 403);
|
||||||
|
}
|
||||||
|
|
||||||
const { rpId, origin } = getAuthenticatorOptions();
|
const { rpId, origin } = getAuthenticatorOptions();
|
||||||
|
|
||||||
const verification = await verifyAuthenticationResponse({
|
const verification = await verifyAuthenticationResponse({
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export const DOCUMENSO_INTERNAL_EMAIL = {
|
|||||||
address: FROM_ADDRESS,
|
address: FROM_ADDRESS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
|
|
||||||
|
|
||||||
export const EMAIL_VERIFICATION_STATE = {
|
export const EMAIL_VERIFICATION_STATE = {
|
||||||
NOT_FOUND: 'NOT_FOUND',
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
VERIFIED: 'VERIFIED',
|
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';
|
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 () => {
|
export const deletedAccountServiceAccount = async () => {
|
||||||
const serviceAccount = await prisma.user.findFirst({
|
const serviceAccount = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: 'deleted-account@documenso.com',
|
email: deletedServiceAccountEmail(),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -29,3 +47,20 @@ export const deletedAccountServiceAccount = async () => {
|
|||||||
|
|
||||||
return serviceAccount;
|
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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user