mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 05:28:51 +00:00
Make merge user step automatic after email verification
This commit is contained in:
@@ -21,9 +21,12 @@ import {
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
@@ -48,6 +51,8 @@ type TSamlConfigServiceFactoryDep = {
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
|
||||
@@ -60,7 +65,9 @@ export const samlConfigServiceFactory = ({
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
tokenService,
|
||||
smtpService
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
@@ -439,6 +446,22 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.EmailVerification,
|
||||
subjectLine: "Infisical confirmation code",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
code: token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
secretApprovalPolicyDAL
|
||||
});
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||
const samlService = samlConfigServiceFactory({
|
||||
permissionService,
|
||||
orgBotDAL,
|
||||
@@ -263,7 +264,9 @@ export const registerRoutes = async (
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
samlConfigDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
tokenService,
|
||||
smtpService
|
||||
});
|
||||
const groupService = groupServiceFactory({
|
||||
userDAL,
|
||||
@@ -333,7 +336,6 @@ export const registerRoutes = async (
|
||||
queueService
|
||||
});
|
||||
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||
const userService = userServiceFactory({
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
|
||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -15,13 +14,15 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
username: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.user.sendEmailVerificationCode(req.permission.id);
|
||||
await server.services.user.sendEmailVerificationCode(req.body.username);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
@@ -34,73 +35,19 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
username: z.string().trim(),
|
||||
code: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.user.verifyEmailVerificationCode(req.permission.id, req.body.code);
|
||||
await server.services.user.verifyEmailVerificationCode(req.body.username, req.body.code);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/users/same-email",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
users: UsersSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const users = await server.services.user.listUsersWithSameEmail(req.permission.id);
|
||||
return {
|
||||
users
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/me/users/merge-user",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
username: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
const appCfg = getConfig();
|
||||
const user = await server.services.user.mergeUsers(req.permission.id, req.body.username);
|
||||
void res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
return {
|
||||
user
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/me/mfa",
|
||||
|
||||
@@ -16,6 +16,7 @@ type TUserServiceFactoryDep = {
|
||||
| "findById"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "update"
|
||||
| "deleteById"
|
||||
| "findOneUserAction"
|
||||
| "createUserAction"
|
||||
@@ -36,8 +37,8 @@ export const userServiceFactory = ({
|
||||
tokenService,
|
||||
smtpService
|
||||
}: TUserServiceFactoryDep) => {
|
||||
const sendEmailVerificationCode = async (userId: string) => {
|
||||
const user = await userDAL.findById(userId);
|
||||
const sendEmailVerificationCode = async (username: string) => {
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||
if (!user.email)
|
||||
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
||||
@@ -59,8 +60,8 @@ export const userServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const verifyEmailVerificationCode = async (userId: string, code: string) => {
|
||||
const user = await userDAL.findById(userId);
|
||||
const verifyEmailVerificationCode = async (username: string, code: string) => {
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||
if (user.isEmailVerified)
|
||||
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
||||
@@ -71,86 +72,65 @@ export const userServiceFactory = ({
|
||||
code
|
||||
});
|
||||
|
||||
await userDAL.updateById(userId, { isEmailVerified: true });
|
||||
};
|
||||
|
||||
// lists users with same verified email only
|
||||
const listUsersWithSameEmail = async (userId: string) => {
|
||||
const user = await userDAL.findById(userId);
|
||||
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||
if (!user.email)
|
||||
throw new BadRequestError({ name: "Failed to list users with same email due to no email on user" });
|
||||
if (!user.isEmailVerified)
|
||||
throw new BadRequestError({ name: "Failed to list users with same email due to email not verified" });
|
||||
|
||||
const users = await userDAL.find({
|
||||
email: user.email,
|
||||
isEmailVerified: true
|
||||
});
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges two users with the same email. Specifically:
|
||||
* - Deletes the current user with id [userId] and transfers any resources to the user with username [username]
|
||||
* @param userId
|
||||
* @param username
|
||||
*/
|
||||
const mergeUsers = async (userId: string, username: string) => {
|
||||
const targetUser = await userDAL.transaction(async (tx) => {
|
||||
const myUser = await userDAL.findById(userId, tx);
|
||||
if (!myUser || !myUser.isEmailVerified) throw new BadRequestError({});
|
||||
|
||||
const mergeUser = await userDAL.findOne(
|
||||
await userDAL.transaction(async (tx) => {
|
||||
await userDAL.updateById(
|
||||
user.id,
|
||||
{
|
||||
username
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!mergeUser || !mergeUser.isEmailVerified) throw new BadRequestError({});
|
||||
|
||||
if (myUser.email !== mergeUser.email) throw new BadRequestError({});
|
||||
|
||||
const mergeUserOrgMembershipSet = new Set(
|
||||
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
|
||||
);
|
||||
const myOrgMemberships = (await orgMembershipDAL.find({ userId: myUser.id }, { tx })).filter(
|
||||
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
|
||||
);
|
||||
|
||||
const userAliases = await userAliasDAL.find(
|
||||
// check if there are users with the same email.
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
userId: myUser.id
|
||||
email: user.email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
await userDAL.deleteById(myUser.id, tx);
|
||||
|
||||
if (myOrgMemberships.length) {
|
||||
await orgMembershipDAL.insertMany(
|
||||
myOrgMemberships.map((orgMembership) => ({
|
||||
...orgMembership,
|
||||
userId: mergeUser.id
|
||||
})),
|
||||
tx
|
||||
if (users.length > 1) {
|
||||
// merge users
|
||||
const mergeUser = users.find((u) => u.id !== user.id);
|
||||
if (!mergeUser) throw new BadRequestError({ name: "Failed to find merge user" });
|
||||
|
||||
const mergeUserOrgMembershipSet = new Set(
|
||||
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
|
||||
);
|
||||
}
|
||||
|
||||
if (userAliases.length) {
|
||||
await userAliasDAL.insertMany(
|
||||
userAliases.map((userAlias) => ({
|
||||
...userAlias,
|
||||
userId: mergeUser.id
|
||||
})),
|
||||
tx
|
||||
const myOrgMemberships = (await orgMembershipDAL.find({ userId: user.id }, { tx })).filter(
|
||||
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
|
||||
);
|
||||
}
|
||||
|
||||
return mergeUser;
|
||||
const userAliases = await userAliasDAL.find(
|
||||
{
|
||||
userId: user.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
await userDAL.deleteById(user.id, tx);
|
||||
|
||||
if (myOrgMemberships.length) {
|
||||
await orgMembershipDAL.insertMany(
|
||||
myOrgMemberships.map((orgMembership) => ({
|
||||
...orgMembership,
|
||||
userId: mergeUser.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (userAliases.length) {
|
||||
await userAliasDAL.insertMany(
|
||||
userAliases.map((userAlias) => ({
|
||||
...userAlias,
|
||||
userId: mergeUser.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return targetUser;
|
||||
};
|
||||
|
||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
||||
@@ -217,8 +197,6 @@ export const userServiceFactory = ({
|
||||
return {
|
||||
sendEmailVerificationCode,
|
||||
verifyEmailVerificationCode,
|
||||
listUsersWithSameEmail,
|
||||
mergeUsers,
|
||||
toggleUserMfa,
|
||||
updateUserName,
|
||||
updateAuthMethods,
|
||||
|
||||
Reference in New Issue
Block a user