feat: implemented backend updates for email casing issue

This commit is contained in:
=
2025-05-15 00:56:52 +05:30
parent 6188de43e4
commit f85add7cca
19 changed files with 304 additions and 89 deletions
@@ -199,9 +199,12 @@ export const authLoginServiceFactory = ({
providerAuthToken,
clientPublicKey
}: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg();
@@ -250,9 +253,12 @@ export const authLoginServiceFactory = ({
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
@@ -649,10 +655,12 @@ export const authLoginServiceFactory = ({
* OAuth2 login for google,github, and other oauth2 provider
* */
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
let user = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(email);
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods) {
if (serverCfg.enabledLoginMethods && user) {
switch (authMethod) {
case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
@@ -715,8 +723,8 @@ export const authLoginServiceFactory = ({
}
user = await userDAL.create({
username: email,
email,
username: email.toLowerCase(),
email: email.toLowerCase(),
isEmailVerified: true,
firstName,
lastName,
@@ -814,11 +822,14 @@ export const authLoginServiceFactory = ({
? decodedProviderToken.orgId
: undefined;
const userEnc = await userDAL.findUserEncKeyByUsername({
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
if (!userEnc.serverEncryptedPrivateKey)
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc?.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
const token = await generateUserTokens({
@@ -121,7 +121,10 @@ export const authPaswordServiceFactory = ({
*/
const sendPasswordResetEmail = async (email: string) => {
const sendEmail = async () => {
const user = await userDAL.findUserByUsername(email);
const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
if (user && user.isAccepted) {
const cfg = getConfig();
@@ -152,7 +155,10 @@ export const authPaswordServiceFactory = ({
* */
const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig();
const user = await userDAL.findUserByUsername(email);
const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
@@ -73,18 +73,27 @@ export const authSignupServiceFactory = ({
}: TAuthSignupDep) => {
// first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => {
const isEmailInvalid = await isDisposableEmail(email);
const sanitizedEmail = email.toLowerCase();
const isEmailInvalid = await isDisposableEmail(sanitizedEmail);
if (isEmailInvalid) {
throw new Error("Provided a disposable email");
}
let user = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
let user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (user && user.isAccepted) {
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account");
throw new BadRequestError({ message: "Failed to send verification code for complete account" });
}
if (!user) {
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false });
user = await userDAL.create({
authMethods: [AuthMethod.EMAIL],
username: sanitizedEmail,
email: sanitizedEmail,
isGhost: false
});
}
if (!user) throw new Error("Failed to create user");
@@ -96,7 +105,7 @@ export const authSignupServiceFactory = ({
await smtpService.sendMail({
template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email as string],
recipients: [sanitizedEmail],
substitutions: {
code: token
}
@@ -104,11 +113,15 @@ export const authSignupServiceFactory = ({
};
const verifyEmailSignup = async (email: string, code: string) => {
const user = await userDAL.findUserByUsername(email);
const sanitizedEmail = email.toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) {
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account");
}
const appCfg = getConfig();
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
@@ -153,12 +166,15 @@ export const authSignupServiceFactory = ({
authorization,
useDefaultOrg
}: TCompleteAccountSignupDTO) => {
const sanitizedEmail = email.toLowerCase();
const appCfg = getConfig();
const serverCfg = await getServerCfg();
const user = await userDAL.findOne({ username: email });
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
throw new BadRequestError({ message: "Failed to complete account for complete user" });
}
let organizationId: string | null = null;
@@ -315,7 +331,7 @@ export const authSignupServiceFactory = ({
}
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted }
);
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
@@ -382,9 +398,9 @@ export const authSignupServiceFactory = ({
* User signup flow when they are invited to join the org
* */
const completeAccountInvite = async ({
email,
ip,
salt,
email,
password,
verifier,
firstName,
@@ -399,7 +415,10 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag,
authorization
}: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email);
const sanitizedEmail = email.toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
}
@@ -407,7 +426,7 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id);
const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email,
inviteEmail: sanitizedEmail,
status: OrgMembershipStatus.Invited
});
if (!orgMembership)
@@ -454,7 +473,7 @@ export const authSignupServiceFactory = ({
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
@@ -505,7 +524,7 @@ export const authSignupServiceFactory = ({
}
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted },
tx
);
+9 -2
View File
@@ -811,7 +811,11 @@ export const orgServiceFactory = ({
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) {
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
let inviteeUser =
usersByUsername?.length > 1
? usersByUsername.find((el) => el.username === inviteeEmail)
: usersByUsername?.[0];
// if the user doesn't exist we create the user with the email
if (!inviteeUser) {
@@ -1223,10 +1227,13 @@ export const orgServiceFactory = ({
* magic link and issue a temporary signup token for user to complete setting up their account
*/
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
const user = await userDAL.findUserByUsername(email);
const usersByUsername = await userDAL.findUserByUsername(email);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!user) {
throw new NotFoundError({ message: "User not found" });
}
const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,
@@ -257,8 +257,8 @@ export const superAdminServiceFactory = ({
const adminSignUp = async ({
lastName,
firstName,
salt,
email,
salt,
password,
verifier,
publicKey,
@@ -272,7 +272,8 @@ export const superAdminServiceFactory = ({
userAgent
}: TAdminSignUpDTO) => {
const appCfg = getConfig();
const existingUser = await userDAL.findOne({ email });
const sanitizedEmail = email.toLowerCase();
const existingUser = await userDAL.findOne({ username: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" });
const privateKey = await getUserPrivateKey(password, {
@@ -292,8 +293,8 @@ export const superAdminServiceFactory = ({
{
firstName,
lastName,
username: email,
email,
username: sanitizedEmail,
email: sanitizedEmail,
superAdmin: true,
isGhost: false,
isAccepted: true,
@@ -348,12 +349,13 @@ export const superAdminServiceFactory = ({
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig();
const sanitizedEmail = email.toLowerCase();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) {
throw new BadRequestError({ message: "Instance has already been set up" });
}
const existingUser = await userDAL.findOne({ email });
const existingUser = await userDAL.findOne({ email: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" });
const userInfo = await userDAL.transaction(async (tx) => {
@@ -361,8 +363,8 @@ export const superAdminServiceFactory = ({
{
firstName: "Admin",
lastName: "User",
username: email,
email,
username: sanitizedEmail,
email: sanitizedEmail,
superAdmin: true,
isGhost: false,
isAccepted: true,
@@ -372,7 +374,7 @@ export const superAdminServiceFactory = ({
tx
);
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password);
const encKeys = await generateUserSrpKeys(sanitizedEmail, password);
const userEnc = await userDAL.createUserEncryption(
{
+5 -5
View File
@@ -17,7 +17,8 @@ export type TUserDALFactory = ReturnType<typeof userDALFactory>;
export const userDALFactory = (db: TDbClient) => {
const userOrm = ormify(db, TableName.Users);
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
const findUserByUsername = async (username: string, tx?: Knex) =>
(tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() });
const getUsersByFilter = async ({
limit,
@@ -41,7 +42,7 @@ export const userDALFactory = (db: TDbClient) => {
.whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`);
.orWhereRaw('lower("username") like ?', `%${searchTerm}%`);
});
}
@@ -65,12 +66,11 @@ export const userDALFactory = (db: TDbClient) => {
try {
return await db
.replicaNode()(TableName.Users)
.whereRaw('lower("username") = :username', { username: username.toLowerCase() })
.where({
username,
isGhost: false
})
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
.first();
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
} catch (error) {
throw new DatabaseError({ error, name: "Find user enc by email" });
}
+35 -8
View File
@@ -21,7 +21,7 @@ type TUserServiceFactoryDep = {
userDAL: Pick<
TUserDALFactory,
| "find"
| "findOne"
| "findUserByUsername"
| "findById"
| "transaction"
| "updateById"
@@ -54,8 +54,11 @@ export const userServiceFactory = ({
permissionService
}: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username });
// akhilmhdh: case sensitive email resolution
const users = await userDAL.findUserByUsername(username);
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified)
@@ -77,7 +80,10 @@ export const userServiceFactory = ({
};
const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username });
// akhilmhdh: case sensitive email resolution
const usersByusername = await userDAL.findUserByUsername(username);
const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
@@ -104,7 +110,7 @@ export const userServiceFactory = ({
// check if there are verified users with the same email.
const users = await userDAL.find(
{
email,
email: email.toLowerCase(),
isEmailVerified: true
},
{ tx }
@@ -163,7 +169,7 @@ export const userServiceFactory = ({
await userDAL.updateById(
user.id,
{
username: email
username: email.toLowerCase()
},
tx
);
@@ -212,6 +218,23 @@ export const userServiceFactory = ({
return updatedUser;
};
const getAllMyAccounts = async (email: string, userId: string) => {
const users = await userDAL.find({ email });
return users?.map((el) => ({ ...el, isMyAccount: el.id === userId }));
};
const removeMyDuplicateAccounts = async (email: string, userId: string) => {
const users = await userDAL.find({ email });
const duplicatedAccounts = users?.filter((el) => el.id !== userId);
const myAccount = users?.find((el) => el.id === userId);
if (duplicatedAccounts.length && myAccount) {
await userDAL.transaction(async (tx) => {
await userDAL.delete({ $in: { id: duplicatedAccounts?.map((el) => el.id) } }, tx);
await userDAL.updateById(userId, { username: myAccount.username.toLowerCase() });
});
}
};
const getMe = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
@@ -313,9 +336,11 @@ export const userServiceFactory = ({
};
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
const user = await userDAL.findOne({
username
});
// akhilmhdh: case sensitive email resolution
const usersByusername = await userDAL.findUserByUsername(username);
const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
if (user.id !== actorId) {
@@ -346,7 +371,9 @@ export const userServiceFactory = ({
getUserAction,
unlockUser,
getUserPrivateKey,
getAllMyAccounts,
getUserProjectFavorites,
removeMyDuplicateAccounts,
updateUserProjectFavorites
};
};