Merge pull request #4344 from Infisical/fix/samlDuplicateAccounts

Fix SAML duplicate accounts when signing in the first time on an existing account
This commit is contained in:
carlosmonastyrski
2025-08-26 23:18:33 +08:00
committed by GitHub
14 changed files with 175 additions and 79 deletions
@@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 1000;
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified"))) {
// Add the column
await knex.schema.alterTable(TableName.UserAliases, (t) => {
t.boolean("isEmailVerified").defaultTo(false);
});
const aliasesToUpdate: { aliasId: string; isEmailVerified: boolean }[] = await knex(TableName.UserAliases)
.join(TableName.Users, `${TableName.UserAliases}.userId`, `${TableName.Users}.id`)
.select([`${TableName.UserAliases}.id as aliasId`, `${TableName.Users}.isEmailVerified`]);
for (let i = 0; i < aliasesToUpdate.length; i += BATCH_SIZE) {
const batch = aliasesToUpdate.slice(i, i + BATCH_SIZE);
const trueIds = batch.filter((row) => row.isEmailVerified).map((row) => row.aliasId);
if (trueIds.length > 0) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.UserAliases).whereIn("id", trueIds).update({ isEmailVerified: true });
}
}
}
if (!(await knex.schema.hasColumn(TableName.AuthTokens, "aliasId"))) {
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.string("aliasId").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified")) {
await knex.schema.alterTable(TableName.UserAliases, (t) => {
t.dropColumn("isEmailVerified");
});
}
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.dropColumn("aliasId");
});
}
}
+2 -1
View File
@@ -17,7 +17,8 @@ export const AuthTokensSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional() orgId: z.string().uuid().nullable().optional(),
aliasId: z.string().nullable().optional()
}); });
export type TAuthTokens = z.infer<typeof AuthTokensSchema>; export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
+2 -1
View File
@@ -16,7 +16,8 @@ export const UserAliasesSchema = z.object({
emails: z.string().array().nullable().optional(), emails: z.string().array().nullable().optional(),
orgId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
isEmailVerified: z.boolean().default(false).nullable().optional()
}); });
export type TUserAliases = z.infer<typeof UserAliasesSchema>; export type TUserAliases = z.infer<typeof UserAliasesSchema>;
@@ -400,15 +400,13 @@ export const ldapConfigServiceFactory = ({
userAlias = await userDAL.transaction(async (tx) => { userAlias = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined; let newUser: TUsers | undefined;
if (serverCfg.trustLdapEmails) { newUser = await userDAL.findOne(
newUser = await userDAL.findOne( {
{ email: email.toLowerCase(),
email: email.toLowerCase(), isEmailVerified: true
isEmailVerified: true },
}, tx
tx );
);
}
if (!newUser) { if (!newUser) {
const uniqueUsername = await normalizeUsername(username, userDAL); const uniqueUsername = await normalizeUsername(username, userDAL);
@@ -433,7 +431,8 @@ export const ldapConfigServiceFactory = ({
aliasType: UserAliasType.LDAP, aliasType: UserAliasType.LDAP,
externalId, externalId,
emails: [email], emails: [email],
orgId orgId,
isEmailVerified: serverCfg.trustLdapEmails
}, },
tx tx
); );
@@ -556,15 +555,14 @@ export const ldapConfigServiceFactory = ({
return newUser; return newUser;
}); });
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
const providerAuthToken = crypto.jwt().sign( const providerAuthToken = crypto.jwt().sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
hasExchangedPrivateKey: true, hasExchangedPrivateKey: true,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), ...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
@@ -572,6 +570,7 @@ export const ldapConfigServiceFactory = ({
organizationSlug: organization.slug, organizationSlug: organization.slug,
authMethod: AuthMethod.LDAP, authMethod: AuthMethod.LDAP,
authType: UserAliasType.LDAP, authType: UserAliasType.LDAP,
aliasId: userAlias.id,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
? { ? {
@@ -585,10 +584,11 @@ export const ldapConfigServiceFactory = ({
} }
); );
if (user.email && !user.isEmailVerified) { if (user.email && !userAlias.isEmailVerified) {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id userId: user.id,
aliasId: userAlias.id
}); });
await smtpService.sendMail({ await smtpService.sendMail({
@@ -180,7 +180,7 @@ export const oidcConfigServiceFactory = ({
} }
const appCfg = getConfig(); const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({ let userAlias = await userAliasDAL.findOne({
externalId, externalId,
orgId, orgId,
aliasType: UserAliasType.OIDC aliasType: UserAliasType.OIDC
@@ -231,32 +231,29 @@ export const oidcConfigServiceFactory = ({
} else { } else {
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined; let newUser: TUsers | undefined;
// we prioritize getting the most complete user to create the new alias under
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
if (serverCfg.trustOidcEmails) { if (!newUser) {
// we prioritize getting the most complete user to create the new alias under // this fetches user entries created via invites
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, username: email
isEmailVerified: true
}, },
tx tx
); );
if (!newUser) { if (newUser && !newUser.isEmailVerified) {
// this fetches user entries created via invites // we automatically mark it as email-verified because we've configured trust for OIDC emails
newUser = await userDAL.findOne( newUser = await userDAL.updateById(newUser.id, {
{ isEmailVerified: serverCfg.trustOidcEmails
username: email });
},
tx
);
if (newUser && !newUser.isEmailVerified) {
// we automatically mark it as email-verified because we've configured trust for OIDC emails
newUser = await userDAL.updateById(newUser.id, {
isEmailVerified: true
});
}
} }
} }
@@ -276,13 +273,14 @@ export const oidcConfigServiceFactory = ({
); );
} }
await userAliasDAL.create( userAlias = await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
aliasType: UserAliasType.OIDC, aliasType: UserAliasType.OIDC,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email] : [],
orgId orgId,
isEmailVerified: serverCfg.trustOidcEmails
}, },
tx tx
); );
@@ -404,19 +402,20 @@ export const oidcConfigServiceFactory = ({
await licenseService.updateSubscriptionOrgMemberCount(organization.id); await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
const providerAuthToken = crypto.jwt().sign( const providerAuthToken = crypto.jwt().sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), ...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug, organizationSlug: organization.slug,
hasExchangedPrivateKey: true, hasExchangedPrivateKey: true,
aliasId: userAlias.id,
authMethod: AuthMethod.OIDC, authMethod: AuthMethod.OIDC,
authType: UserAliasType.OIDC, authType: UserAliasType.OIDC,
isUserCompleted, isUserCompleted,
@@ -430,10 +429,11 @@ export const oidcConfigServiceFactory = ({
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() }); await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) { if (user.email && !userAlias.isEmailVerified) {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id userId: user.id,
aliasId: userAlias.id
}); });
await smtpService await smtpService
@@ -246,7 +246,7 @@ export const samlConfigServiceFactory = ({
}); });
} }
const userAlias = await userAliasDAL.findOne({ let userAlias = await userAliasDAL.findOne({
externalId, externalId,
orgId, orgId,
aliasType: UserAliasType.SAML aliasType: UserAliasType.SAML
@@ -320,15 +320,13 @@ export const samlConfigServiceFactory = ({
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined; let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) { newUser = await userDAL.findOne(
newUser = await userDAL.findOne( {
{ email,
email, isEmailVerified: true
isEmailVerified: true },
}, tx
tx );
);
}
if (!newUser) { if (!newUser) {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL); const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
@@ -346,13 +344,14 @@ export const samlConfigServiceFactory = ({
); );
} }
await userAliasDAL.create( userAlias = await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
aliasType: UserAliasType.SAML, aliasType: UserAliasType.SAML,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email] : [],
orgId orgId,
isEmailVerified: serverCfg.trustSamlEmails
}, },
tx tx
); );
@@ -410,13 +409,13 @@ export const samlConfigServiceFactory = ({
} }
await licenseService.updateSubscriptionOrgMemberCount(organization.id); await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified); const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified && userAlias.isEmailVerified);
const providerAuthToken = crypto.jwt().sign( const providerAuthToken = crypto.jwt().sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), ...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
@@ -424,6 +423,7 @@ export const samlConfigServiceFactory = ({
organizationSlug: organization.slug, organizationSlug: organization.slug,
authMethod: authProvider, authMethod: authProvider,
hasExchangedPrivateKey: true, hasExchangedPrivateKey: true,
aliasId: userAlias.id,
authType: UserAliasType.SAML, authType: UserAliasType.SAML,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
@@ -440,10 +440,11 @@ export const samlConfigServiceFactory = ({
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() }); await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) { if (user.email && !userAlias.isEmailVerified) {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id userId: user.id,
aliasId: userAlias.id
}); });
await smtpService.sendMail({ await smtpService.sendMail({
+2 -1
View File
@@ -726,7 +726,8 @@ export const registerRoutes = async (
permissionService, permissionService,
groupProjectDAL, groupProjectDAL,
smtpService, smtpService,
projectMembershipDAL projectMembershipDAL,
userAliasDAL
}); });
const totpService = totpServiceFactory({ const totpService = totpServiceFactory({
+2 -2
View File
@@ -18,14 +18,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
username: z.string().trim() token: z.string().trim()
}), }),
response: { response: {
200: z.object({}) 200: z.object({})
} }
}, },
handler: async (req) => { handler: async (req) => {
await server.services.user.sendEmailVerificationCode(req.body.username); await server.services.user.sendEmailVerificationCode(req.body.token);
return {}; return {};
} }
}); });
@@ -75,7 +75,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
}; };
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => { const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS); const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
@@ -88,7 +88,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
type, type,
userId, userId,
orgId, orgId,
triesLeft: tkCfg?.triesLeft triesLeft: tkCfg?.triesLeft,
aliasId
}, },
tx tx
); );
@@ -14,6 +14,7 @@ export type TCreateTokenForUserDTO = {
type: TokenType; type: TokenType;
userId: string; userId: string;
orgId?: string; orgId?: string;
aliasId?: string;
}; };
export type TCreateOrgInviteTokenDTO = { export type TCreateOrgInviteTokenDTO = {
@@ -453,6 +453,13 @@ export const authLoginServiceFactory = ({
const selectedOrg = await orgDAL.findById(organizationId); const selectedOrg = await orgDAL.findById(organizationId);
// Check if authEnforced is true, if that's the case, throw an error
if (selectedOrg.authEnforced) {
throw new BadRequestError({
message: "Authentication is required by your organization before you can log in."
});
}
if (!selectedOrgMembership) { if (!selectedOrgMembership) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `User does not have access to the organization named ${selectedOrg?.name}` message: `User does not have access to the organization named ${selectedOrg?.name}`
+35 -10
View File
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -9,9 +10,10 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { TUserDALFactory } from "./user-dal"; import { TUserDALFactory } from "./user-dal";
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types"; import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
@@ -37,6 +39,7 @@ type TUserServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "find" | "updateById">;
}; };
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>; export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
@@ -48,22 +51,38 @@ export const userServiceFactory = ({
groupProjectDAL, groupProjectDAL,
tokenService, tokenService,
smtpService, smtpService,
permissionService permissionService,
userAliasDAL
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => { const sendEmailVerificationCode = async (token: string) => {
const { authType, aliasId, username, authTokenType } = crypto.jwt().decode(token) as {
authType: string;
aliasId?: string;
username: string;
authTokenType: AuthTokenType;
};
if (authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new BadRequestError({ name: "Invalid auth token type" });
// akhilmhdh: case sensitive email resolution // akhilmhdh: case sensitive email resolution
const users = await userDAL.findUserByUsername(username); const users = await userDAL.findUserByUsername(username);
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0]; 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) throw new NotFoundError({ name: `User with username '${username}' not found` });
let { isEmailVerified } = user;
if (aliasId) {
const userAlias = await userAliasDAL.findOne({ userId: user.id, aliasType: authType, id: aliasId });
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${aliasId}' not found` });
isEmailVerified = userAlias.isEmailVerified;
}
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified) if (isEmailVerified)
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" }); throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
const token = await tokenService.createTokenForUser({ const userToken = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id userId: user.id,
aliasId
}); });
await smtpService.sendMail({ await smtpService.sendMail({
@@ -71,7 +90,7 @@ export const userServiceFactory = ({
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
recipients: [user.email], recipients: [user.email],
substitutions: { substitutions: {
code: token code: userToken
} }
}); });
}; };
@@ -95,15 +114,21 @@ export const userServiceFactory = ({
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await tokenService.validateTokenForUser({ const token = await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id, userId: user.id,
code code
}); });
if (token?.aliasId) {
const userAlias = await userAliasDAL.findOne({ userId: user.id, id: token.aliasId });
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${token.aliasId}' not found` });
if (userAlias?.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await userAliasDAL.updateById(token.aliasId, { isEmailVerified: true });
}
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : []; const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
await userDAL.updateById(user.id, { await userDAL.updateById(user.id, {
+4 -4
View File
@@ -26,16 +26,16 @@ export const useAddUserToWsNonE2EE = () => {
}); });
}; };
export const sendEmailVerificationCode = async (username: string) => { export const sendEmailVerificationCode = async (token: string) => {
return apiRequest.post("/api/v2/users/me/emails/code", { return apiRequest.post("/api/v2/users/me/emails/code", {
username token
}); });
}; };
export const useSendEmailVerificationCode = () => { export const useSendEmailVerificationCode = () => {
return useMutation({ return useMutation({
mutationFn: async (username: string) => { mutationFn: async (token: string) => {
await sendEmailVerificationCode(username); await sendEmailVerificationCode(token);
return {}; return {};
} }
}); });
@@ -114,7 +114,16 @@ export const EmailConfirmationStep = ({
const resendCode = async () => { const resendCode = async () => {
try { try {
await sendEmailVerificationCode(username); const queryParams = new URLSearchParams(window.location.search);
const token = queryParams.get("token");
if (!token) {
createNotification({
text: "Failed to resend code, no token found",
type: "error"
});
return;
}
await sendEmailVerificationCode(token);
createNotification({ createNotification({
text: "Successfully resent code", text: "Successfully resent code",
type: "success" type: "success"