diff --git a/backend/src/db/migrations/20240426162819_user-alias-optional-username.ts b/backend/src/db/migrations/20240426162819_user-alias-optional-username.ts new file mode 100644 index 000000000..380660149 --- /dev/null +++ b/backend/src/db/migrations/20240426162819_user-alias-optional-username.ts @@ -0,0 +1,11 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.UserAliases, (t) => { + t.string("username").nullable().alter(); + }); +} + +export async function down(): Promise {} diff --git a/backend/src/db/schemas/user-aliases.ts b/backend/src/db/schemas/user-aliases.ts index d8712fe75..14147abf8 100644 --- a/backend/src/db/schemas/user-aliases.ts +++ b/backend/src/db/schemas/user-aliases.ts @@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models"; export const UserAliasesSchema = z.object({ id: z.string().uuid(), userId: z.string().uuid(), - username: z.string(), + username: z.string().nullable().optional(), aliasType: z.string(), externalId: z.string(), emails: z.string().array().nullable().optional(), diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 4f5bebec8..81543c85e 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -99,7 +99,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { // eslint-disable-next-line async (req, profile, cb) => { try { - console.log("saml login profile: ", profile); if (!profile) throw new BadRequestError({ message: "Missing profile" }); const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved @@ -109,7 +108,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ externalId: profile.nameID, - username: profile.nameID ?? email, email, firstName: profile.firstName as string, lastName: profile.lastName as string, diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index c144a42cd..d6fa72c27 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -31,7 +31,7 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal import { TUserDALFactory } from "@app/services/user/user-dal"; import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; -import { UserAliasType } from "@app/services/user-alias/user-alias-types"; +import { TUserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -395,7 +395,7 @@ export const ldapConfigServiceFactory = ({ let userAlias = await userAliasDAL.findOne({ externalId, orgId, - aliasType: UserAliasType.LDAP + aliasType: TUserAliasType.LDAP }); const organization = await orgDAL.findOrgById(orgId); @@ -437,9 +437,10 @@ export const ldapConfigServiceFactory = ({ { username: uniqueUsername, email: emails[0], + isEmailVerified: false, firstName, lastName, - authMethods: [AuthMethod.LDAP], // should this be empty? + authMethods: [], isGhost: false }, tx @@ -448,7 +449,7 @@ export const ldapConfigServiceFactory = ({ { userId: newUser.id, username, - aliasType: UserAliasType.LDAP, + aliasType: TUserAliasType.LDAP, externalId, emails, orgId @@ -556,11 +557,14 @@ export const ldapConfigServiceFactory = ({ authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, username: user.username, + ...(user.email && { email: user.email }), firstName, lastName, organizationName: organization.name, organizationId: organization.id, + organizationSlug: organization.slug, authMethod: AuthMethod.LDAP, + authType: TUserAliasType.LDAP, isUserCompleted, ...(relayState ? { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 9a2de2f00..a4785132b 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -23,10 +23,11 @@ import { BadRequestError } from "@app/lib/errors"; import { AuthTokenType } from "@app/services/auth/auth-type"; 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 { TUserDALFactory } from "@app/services/user/user-dal"; import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; -import { UserAliasType } from "@app/services/user-alias/user-alias-types"; +import { TUserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -35,13 +36,14 @@ import { TSamlConfigDALFactory } from "./saml-config-dal"; import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types"; type TSamlConfigServiceFactoryDep = { - samlConfigDAL: TSamlConfigDALFactory; + samlConfigDAL: TSamlConfigDALFactory; // TODO: Pick userDAL: Pick; userAliasDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; + orgMembershipDAL: TOrgMembershipDALFactory; // TODO: Pick orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -53,6 +55,7 @@ export const samlConfigServiceFactory = ({ samlConfigDAL, orgBotDAL, orgDAL, + orgMembershipDAL, userDAL, userAliasDAL, permissionService, @@ -312,7 +315,6 @@ export const samlConfigServiceFactory = ({ const samlLogin = async ({ externalId, - username, // what to do about this? email, firstName, lastName, @@ -320,31 +322,18 @@ export const samlConfigServiceFactory = ({ orgId, relayState }: TSamlLoginDTO) => { - console.log("samlLogin args: ", { - externalId, - username, - email, - firstName, - lastName, - authProvider, - orgId, - relayState - }); const appCfg = getConfig(); const userAlias = await userAliasDAL.findOne({ externalId, orgId, - aliasType: UserAliasType.SAML + aliasType: TUserAliasType.SAML }); - console.log("found userAlias: ", userAlias); - const organization = await orgDAL.findOrgById(orgId); if (!organization) throw new BadRequestError({ message: "Org not found" }); let user: TUsers; if (userAlias) { - console.log("samlLogin A"); user = await userDAL.transaction(async (tx) => { const foundUser = await userDAL.findById(userAlias.userId, tx); const [orgMembership] = await orgDAL.findMembership( @@ -355,9 +344,10 @@ export const samlConfigServiceFactory = ({ { tx } ); if (!orgMembership) { - await orgDAL.createMembership( + await orgMembershipDAL.create( { userId: userAlias.userId, + inviteEmail: email, orgId, role: OrgMembershipRole.Member, status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later @@ -365,7 +355,7 @@ export const samlConfigServiceFactory = ({ tx ); // Only update the membership to Accepted if the user account is already completed. - } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { + } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { await orgDAL.updateMembershipById( orgMembership.id, { @@ -378,9 +368,8 @@ export const samlConfigServiceFactory = ({ return foundUser; }); } else { - console.log("samlLogin B"); user = await userDAL.transaction(async (tx) => { - const uniqueUsername = await normalizeUsername(username, userDAL); + const uniqueUsername = await normalizeUsername(externalId, userDAL); const newUser = await userDAL.create( { username: uniqueUsername, @@ -396,8 +385,7 @@ export const samlConfigServiceFactory = ({ await userAliasDAL.create( { userId: newUser.id, - username, - aliasType: UserAliasType.SAML, + aliasType: TUserAliasType.SAML, externalId, emails: email ? [email] : [], orgId @@ -405,10 +393,10 @@ export const samlConfigServiceFactory = ({ tx ); - await orgDAL.createMembership( - // note: this creates a duplicate membership atm + await orgMembershipDAL.create( { userId: newUser.id, + inviteEmail: email, orgId, role: OrgMembershipRole.Member, status: OrgMembershipStatus.Invited @@ -419,7 +407,6 @@ export const samlConfigServiceFactory = ({ return newUser; }); } - console.log("samlLogin C"); const isUserCompleted = Boolean(user.isAccepted); const providerAuthToken = jwt.sign( @@ -432,7 +419,9 @@ export const samlConfigServiceFactory = ({ lastName, organizationName: organization.name, organizationId: organization.id, + organizationSlug: organization.slug, authMethod: authProvider, + authType: TUserAliasType.SAML, isUserCompleted, ...(relayState ? { diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index e7c1a5674..0e84ff666 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -46,7 +46,6 @@ export type TGetSamlCfgDTO = export type TSamlLoginDTO = { externalId: string; - username: string; email?: string; firstName: string; lastName?: string; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9f63d416a..299319e41 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -259,6 +259,7 @@ export const registerRoutes = async ( permissionService, orgBotDAL, orgDAL, + orgMembershipDAL, userDAL, userAliasDAL, samlConfigDAL, @@ -334,7 +335,6 @@ export const registerRoutes = async ( const userService = userServiceFactory({ userDAL, userAliasDAL, - orgDAL, orgMembershipDAL, tokenService, smtpService diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 13c8b97cb..86693df8d 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -135,6 +135,11 @@ export const authSignupServiceFactory = ({ userAgent, authorization }: TCompleteAccountSignupDTO) => { + console.log("completeEmailAccountSignup args: ", { + email, + firstName, + lastName + }); const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); @@ -169,9 +174,8 @@ export const authSignupServiceFactory = ({ tx ); // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it - if (isAuthMethodSaml(authMethod) && organizationId) { + if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) { const [pendingOrgMembership] = await orgDAL.findMembership({ - inviteEmail: email, userId: user.id, status: OrgMembershipStatus.Invited, orgId: organizationId diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 07fc2e991..bec8f3f37 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({ superAdmin: true, isGhost: false, isAccepted: true, - authMethods: [AuthMethod.EMAIL] + authMethods: [AuthMethod.EMAIL], + isEmailVerified: true }, tx ); diff --git a/backend/src/services/user-alias/user-alias-types.ts b/backend/src/services/user-alias/user-alias-types.ts index 09204644f..6188732c6 100644 --- a/backend/src/services/user-alias/user-alias-types.ts +++ b/backend/src/services/user-alias/user-alias-types.ts @@ -1,4 +1,4 @@ -export enum UserAliasType { +export enum TUserAliasType { LDAP = "ldap", SAML = "saml" } diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 552ddff11..5fc22ba38 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,7 +1,6 @@ import { BadRequestError } from "@app/lib/errors"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; -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 { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -9,14 +8,23 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { AuthMethod } from "../auth/auth-type"; import { TUserDALFactory } from "./user-dal"; -// TODO: Pick all of these type TUserServiceFactoryDep = { - userDAL: TUserDALFactory; - userAliasDAL: TUserAliasDALFactory; - orgDAL: TOrgDALFactory; - orgMembershipDAL: TOrgMembershipDALFactory; - tokenService: TAuthTokenServiceFactory; - smtpService: TSmtpService; + userDAL: Pick< + TUserDALFactory, + | "find" + | "findOne" + | "findById" + | "transaction" + | "updateById" + | "deleteById" + | "findOneUserAction" + | "createUserAction" + | "findUserEncKeyByUserId" + >; + userAliasDAL: Pick; + orgMembershipDAL: Pick; + tokenService: Pick; + smtpService: Pick; }; export type TUserServiceFactory = ReturnType; @@ -24,13 +32,11 @@ export type TUserServiceFactory = ReturnType; export const userServiceFactory = ({ userDAL, userAliasDAL, - // orgDAL, orgMembershipDAL, tokenService, smtpService }: TUserServiceFactoryDep) => { const sendEmailVerificationCode = async (userId: string) => { - console.log("sendEmailVerificationCode userId: ", userId); const user = await userDAL.findById(userId); if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user.email) @@ -38,13 +44,11 @@ export const userServiceFactory = ({ if (user.isEmailVerified) throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" }); - console.log("sendEmailVerificationCode user: ", user); const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_VERIFICATION, userId: user.id }); - console.log("sendEmailVerificationCode 2"); await smtpService.sendMail({ template: SmtpTemplates.EmailVerification, subjectLine: "Infisical confirmation code", @@ -56,11 +60,6 @@ export const userServiceFactory = ({ }; const verifyEmailVerificationCode = async (userId: string, code: string) => { - console.log("verifyEmailVerificationCode args: ", { - userId, - code - }); - const user = await userDAL.findById(userId); if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (user.isEmailVerified) diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 67340780d..68ba1c497 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -6,6 +6,7 @@ export const publicPaths = [ "/signup", "/signup/sso", "/login", + "/login/ldap", "/blog", "/docs", "/changelog", diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index 27f4d30e4..72ad7ea6b 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -3,7 +3,8 @@ export { useAddUserToWsNonE2EE, useMergeUsers, useSendEmailVerificationCode, - useVerifyEmailVerificationCode} from "./mutation"; + useVerifyEmailVerificationCode +} from "./mutation"; export { fetchOrgUsers, fetchUsersWithMyEmail, @@ -26,4 +27,5 @@ export { useRevokeMySessions, useUpdateMfaEnabled, useUpdateOrgUserRole, - useUpdateUserAuthMethods} from "./queries"; + useUpdateUserAuthMethods +} from "./queries"; diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 9bebd84cd..1f968ec07 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -5,6 +5,7 @@ import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { apiRequest } from "@app/config/request"; +import { setAuthToken } from "@app/reactQuery"; import { workspaceKeys } from "../workspace/queries"; import { userKeys } from "./queries"; @@ -63,10 +64,14 @@ export const useAddUserToWsNonE2EE = () => { }); }; +export const sendEmailVerificationCode = async () => { + return apiRequest.post("/api/v2/users/me/emails/code"); +}; + export const useSendEmailVerificationCode = () => { return useMutation({ mutationFn: async () => { - await apiRequest.post("/api/v2/users/me/emails/code"); + await sendEmailVerificationCode(); return {}; } }); @@ -88,12 +93,29 @@ export const useVerifyEmailVerificationCode = () => { }; export const useMergeUsers = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ username }: { username: string }) => { const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", { username }); return data; + }, + onSuccess: () => { + setAuthToken(""); + // Delete the cookie by not setting a value; Alternatively clear the local storage + localStorage.removeItem("protectedKey"); + localStorage.removeItem("protectedKeyIV"); + localStorage.removeItem("protectedKeyTag"); + localStorage.removeItem("publicKey"); + localStorage.removeItem("encryptedPrivateKey"); + localStorage.removeItem("iv"); + localStorage.removeItem("tag"); + localStorage.removeItem("PRIVATE_KEY"); + localStorage.removeItem("orgData.id"); + localStorage.removeItem("projectData.id"); + + queryClient.clear(); } }); }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 572296e75..649af434c 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -27,6 +27,11 @@ export type User = { id: string; }; +export enum UserAliasType { + LDAP = "ldap", + SAML = "saml" +} + export type UserEnc = { encryptionVersion?: number; protectedKey?: string; diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx index dd8068dd2..8fdb23f69 100644 --- a/frontend/src/pages/login/index.tsx +++ b/frontend/src/pages/login/index.tsx @@ -7,7 +7,6 @@ import { Login } from "@app/views/Login"; export default function LoginPage() { const { t } = useTranslation(); - return (
diff --git a/frontend/src/pages/login/ldap/index.tsx b/frontend/src/pages/login/ldap/index.tsx new file mode 100644 index 000000000..5d5300e6f --- /dev/null +++ b/frontend/src/pages/login/ldap/index.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; + +import { LoginLDAP } from "@app/views/Login"; + +export default function LoginLDAPPage() { + const { t } = useTranslation(); + return ( +
+ + {t("common.head-title", { title: t("login.title") })} + + + + + + +
+ Infisical logo +
+ + +
+ ); +} diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index ac56c28c3..04a24d233 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { isLoggedIn } from "@app/reactQuery"; -import { InitialStep, LDAPStep, MFAStep, SAMLSSOStep } from "./components"; +import { InitialStep, MFAStep, SAMLSSOStep } from "./components"; import { navigateUserToSelectOrg } from "./Login.utils"; export const Login = () => { @@ -58,8 +58,6 @@ export const Login = () => { ); case 2: return ; - case 3: - return ; default: return
; } diff --git a/frontend/src/views/Login/components/LDAPStep/LDAPStep.tsx b/frontend/src/views/Login/LoginLDAP.tsx similarity index 88% rename from frontend/src/views/Login/components/LDAPStep/LDAPStep.tsx rename to frontend/src/views/Login/LoginLDAP.tsx index e23f16e04..021ac7334 100644 --- a/frontend/src/views/Login/components/LDAPStep/LDAPStep.tsx +++ b/frontend/src/views/Login/LoginLDAP.tsx @@ -1,24 +1,23 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { useRouter } from "next/router"; import { createNotification } from "@app/components/notifications"; import { Button, Input } from "@app/components/v2"; import { loginLDAPRedirect } from "@app/hooks/api/auth/queries"; -type Props = { - setStep: (step: number) => void; -}; +export const LoginLDAP = () => { + const router = useRouter(); + const queryParams = new URLSearchParams(window.location.search); + const passedOrgSlug = queryParams.get("organizationSlug"); + const passedUsername = queryParams.get("username"); -export const LDAPStep = ({ setStep }: Props) => { - - const [organizationSlug, setOrganizationSlug] = useState(""); - const [username, setUsername] = useState(""); + const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || ""); + const [username, setUsername] = useState(passedUsername || ""); const [password, setPassword] = useState(""); const { t } = useTranslation(); - // const queryParams = new URLSearchParams(window.location.search); - const handleSubmission = async (e: React.FormEvent) => { e.preventDefault(); try { @@ -42,7 +41,6 @@ export const LDAPStep = ({ setStep }: Props) => { type: "success" }); - // redirects either to /login/sso or /signup/sso window.open(nextUrl); window.close(); } catch (err) { @@ -76,6 +74,7 @@ export const LDAPStep = ({ setStep }: Props) => { autoComplete="email" id="email" className="h-12" + isDisabled={passedOrgSlug !== null} />
@@ -90,6 +89,7 @@ export const LDAPStep = ({ setStep }: Props) => { autoComplete="email" id="email" className="h-12" + isDisabled={passedUsername !== null} /> @@ -122,7 +122,7 @@ export const LDAPStep = ({ setStep }: Props) => {
)} diff --git a/frontend/src/views/Signup/SignupSSO.tsx b/frontend/src/views/Signup/SignupSSO.tsx index d56a26347..09c707629 100644 --- a/frontend/src/views/Signup/SignupSSO.tsx +++ b/frontend/src/views/Signup/SignupSSO.tsx @@ -5,7 +5,8 @@ import { BackupPDFStep, EmailConfirmationStep, MergeUsersStep, - UserInfoSSOStep} from "./components"; + UserInfoSSOStep +} from "./components"; type Props = { providerAuthToken: string; @@ -15,9 +16,8 @@ export const SignupSSO = ({ providerAuthToken }: Props) => { const [step, setStep] = useState(0); const [password, setPassword] = useState(""); - const { username, email, organizationName, firstName, lastName } = jwt_decode( - providerAuthToken - ) as any; + const { username, email, organizationName, organizationSlug, firstName, lastName, authType } = + jwt_decode(providerAuthToken) as any; const renderView = () => { switch (step) { @@ -37,7 +37,13 @@ export const SignupSSO = ({ providerAuthToken }: Props) => { case 1: return ; case 2: - return ; + return ( + + ); case 3: return ( diff --git a/frontend/src/views/Signup/components/EmailConfirmationStep/EmailConfirmationStep.tsx b/frontend/src/views/Signup/components/EmailConfirmationStep/EmailConfirmationStep.tsx index c22db1ee0..f3a36c8dc 100644 --- a/frontend/src/views/Signup/components/EmailConfirmationStep/EmailConfirmationStep.tsx +++ b/frontend/src/views/Signup/components/EmailConfirmationStep/EmailConfirmationStep.tsx @@ -6,11 +6,11 @@ import ReactCodeInput from "react-code-input"; import Error from "@app/components/basic/Error"; import { createNotification } from "@app/components/notifications"; import { Button } from "@app/components/v2"; -import { useUser } from "@app/context"; import { fetchUsersWithMyEmail, useSendEmailVerificationCode, - useVerifyEmailVerificationCode} from "@app/hooks/api"; + useVerifyEmailVerificationCode +} from "@app/hooks/api"; type Props = { email: string; @@ -56,7 +56,6 @@ const propsPhone = { } as const; export const EmailConfirmationStep = ({ email, setStep }: Props) => { - const { user } = useUser(); const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(false); const [isResendingVerificationEmail] = useState(false); @@ -106,9 +105,6 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {

We've sent a verification code to {email}

-

- {user?.email} -

{ +export const MergeUsersStep = ({ username, authType, organizationSlug }: Props) => { const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [targetUsername, setTargetUsername] = useState(""); const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail(); const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers(); - const handleMergeUser = async (targetUsername: string) => { + const handleMergeUser = async (mergeWithUsername: string) => { try { - console.log("merge A"); - await mergeUser({ username: targetUsername }); - // TODO: logout, make user re-login - console.log("merge B"); + if (!mergeWithUsername) return; + await mergeUser({ username: mergeWithUsername }); createNotification({ text: "Successfully merged user", type: "success" }); - router.push("/login"); + setIsOpen(false); + + switch (authType) { + case UserAliasType.SAML: { + window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`); + window.close(); + break; + } + case UserAliasType.LDAP: { + router.push(`/login/ldap?organizationSlug=${organizationSlug}`); + break; + } + default: { + router.push("/login"); + break; + } + } + + setTargetUsername(""); } catch (err) { console.error(err); createNotification({ @@ -72,14 +96,16 @@ export const MergeUsersStep = ({ username }: Props) => { return ( {`${user.firstName ?? ""} ${user.lastName ?? ""}`} - {username} + {user.username} @@ -97,6 +123,35 @@ export const MergeUsersStep = ({ username }: Props) => { + + +

+ The merge operation will transfer / consolidate your existing organization membership to + the target user you're merging with. +

+

+ If the target user is not yet part of the same organization, then they will be added to + it under your current organization membership. Conversely, if the target user is already + part of the organization, then their existing organization membership will remain. +

+

+ Once the merge operation is complete, you'll be prompted to re-login. +

+
+ + +
+
+
); }; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index d15e57f47..6255ae386 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -17,6 +17,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { sendEmailVerificationCode } from "@app/hooks/api/users/mutation"; import ProjectService from "@app/services/ProjectService"; // eslint-disable-next-line new-cap @@ -205,10 +206,11 @@ export const UserInfoSSOStep = ({ if (email) { // move to verify email + await sendEmailVerificationCode(); setStep(1); } else { // move to backup PDF step - setStep(2); + setStep(3); } } catch (error) { setIsLoading(false);