Finish preliminary email validation, merge user flow w saml/ldap

This commit is contained in:
Tuan Dang
2024-04-26 20:19:43 -07:00
parent d0cb06d875
commit 858a35812a
28 changed files with 225 additions and 104 deletions
@@ -0,0 +1,11 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserAliases, (t) => {
t.string("username").nullable().alter();
});
}
export async function down(): Promise<void> {}
+1 -1
View File
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const UserAliasesSchema = z.object({ export const UserAliasesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
userId: z.string().uuid(), userId: z.string().uuid(),
username: z.string(), username: z.string().nullable().optional(),
aliasType: z.string(), aliasType: z.string(),
externalId: z.string(), externalId: z.string(),
emails: z.string().array().nullable().optional(), emails: z.string().array().nullable().optional(),
-2
View File
@@ -99,7 +99,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line // eslint-disable-next-line
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
console.log("saml login profile: ", profile);
if (!profile) throw new BadRequestError({ message: "Missing 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 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({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
username: profile.nameID ?? email,
email, email,
firstName: profile.firstName as string, firstName: profile.firstName as string,
lastName: profile.lastName as string, lastName: profile.lastName as string,
@@ -31,7 +31,7 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns"; import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; 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 { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -395,7 +395,7 @@ export const ldapConfigServiceFactory = ({
let userAlias = await userAliasDAL.findOne({ let userAlias = await userAliasDAL.findOne({
externalId, externalId,
orgId, orgId,
aliasType: UserAliasType.LDAP aliasType: TUserAliasType.LDAP
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@@ -437,9 +437,10 @@ export const ldapConfigServiceFactory = ({
{ {
username: uniqueUsername, username: uniqueUsername,
email: emails[0], email: emails[0],
isEmailVerified: false,
firstName, firstName,
lastName, lastName,
authMethods: [AuthMethod.LDAP], // should this be empty? authMethods: [],
isGhost: false isGhost: false
}, },
tx tx
@@ -448,7 +449,7 @@ export const ldapConfigServiceFactory = ({
{ {
userId: newUser.id, userId: newUser.id,
username, username,
aliasType: UserAliasType.LDAP, aliasType: TUserAliasType.LDAP,
externalId, externalId,
emails, emails,
orgId orgId
@@ -556,11 +557,14 @@ export const ldapConfigServiceFactory = ({
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 }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: AuthMethod.LDAP, authMethod: AuthMethod.LDAP,
authType: TUserAliasType.LDAP,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
? { ? {
@@ -23,10 +23,11 @@ import { BadRequestError } from "@app/lib/errors";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-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 { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns"; import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; 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 { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; 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"; import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
type TSamlConfigServiceFactoryDep = { type TSamlConfigServiceFactoryDep = {
samlConfigDAL: TSamlConfigDALFactory; samlConfigDAL: TSamlConfigDALFactory; // TODO: Pick
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">; userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">; userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
orgMembershipDAL: TOrgMembershipDALFactory; // TODO: Pick
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -53,6 +55,7 @@ export const samlConfigServiceFactory = ({
samlConfigDAL, samlConfigDAL,
orgBotDAL, orgBotDAL,
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL, userAliasDAL,
permissionService, permissionService,
@@ -312,7 +315,6 @@ export const samlConfigServiceFactory = ({
const samlLogin = async ({ const samlLogin = async ({
externalId, externalId,
username, // what to do about this?
email, email,
firstName, firstName,
lastName, lastName,
@@ -320,31 +322,18 @@ export const samlConfigServiceFactory = ({
orgId, orgId,
relayState relayState
}: TSamlLoginDTO) => { }: TSamlLoginDTO) => {
console.log("samlLogin args: ", {
externalId,
username,
email,
firstName,
lastName,
authProvider,
orgId,
relayState
});
const appCfg = getConfig(); const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({ const userAlias = await userAliasDAL.findOne({
externalId, externalId,
orgId, orgId,
aliasType: UserAliasType.SAML aliasType: TUserAliasType.SAML
}); });
console.log("found userAlias: ", userAlias);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
let user: TUsers; let user: TUsers;
if (userAlias) { if (userAlias) {
console.log("samlLogin A");
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const foundUser = await userDAL.findById(userAlias.userId, tx); const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
@@ -355,9 +344,10 @@ export const samlConfigServiceFactory = ({
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, 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 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 tx
); );
// Only update the membership to Accepted if the user account is already completed. // 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( await orgDAL.updateMembershipById(
orgMembership.id, orgMembership.id,
{ {
@@ -378,9 +368,8 @@ export const samlConfigServiceFactory = ({
return foundUser; return foundUser;
}); });
} else { } else {
console.log("samlLogin B");
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL); const uniqueUsername = await normalizeUsername(externalId, userDAL);
const newUser = await userDAL.create( const newUser = await userDAL.create(
{ {
username: uniqueUsername, username: uniqueUsername,
@@ -396,8 +385,7 @@ export const samlConfigServiceFactory = ({
await userAliasDAL.create( await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
username, aliasType: TUserAliasType.SAML,
aliasType: UserAliasType.SAML,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email] : [],
orgId orgId
@@ -405,10 +393,10 @@ export const samlConfigServiceFactory = ({
tx tx
); );
await orgDAL.createMembership( await orgMembershipDAL.create(
// note: this creates a duplicate membership atm
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited status: OrgMembershipStatus.Invited
@@ -419,7 +407,6 @@ export const samlConfigServiceFactory = ({
return newUser; return newUser;
}); });
} }
console.log("samlLogin C");
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
@@ -432,7 +419,9 @@ export const samlConfigServiceFactory = ({
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider, authMethod: authProvider,
authType: TUserAliasType.SAML,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
? { ? {
@@ -46,7 +46,6 @@ export type TGetSamlCfgDTO =
export type TSamlLoginDTO = { export type TSamlLoginDTO = {
externalId: string; externalId: string;
username: string;
email?: string; email?: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
+1 -1
View File
@@ -259,6 +259,7 @@ export const registerRoutes = async (
permissionService, permissionService,
orgBotDAL, orgBotDAL,
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL, userAliasDAL,
samlConfigDAL, samlConfigDAL,
@@ -334,7 +335,6 @@ export const registerRoutes = async (
const userService = userServiceFactory({ const userService = userServiceFactory({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
orgDAL,
orgMembershipDAL, orgMembershipDAL,
tokenService, tokenService,
smtpService smtpService
@@ -135,6 +135,11 @@ export const authSignupServiceFactory = ({
userAgent, userAgent,
authorization authorization
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
console.log("completeEmailAccountSignup args: ", {
email,
firstName,
lastName
});
const user = await userDAL.findOne({ username: email }); const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
@@ -169,9 +174,8 @@ export const authSignupServiceFactory = ({
tx 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 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({ const [pendingOrgMembership] = await orgDAL.findMembership({
inviteEmail: email,
userId: user.id, userId: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
orgId: organizationId orgId: organizationId
@@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
authMethods: [AuthMethod.EMAIL] authMethods: [AuthMethod.EMAIL],
isEmailVerified: true
}, },
tx tx
); );
@@ -1,4 +1,4 @@
export enum UserAliasType { export enum TUserAliasType {
LDAP = "ldap", LDAP = "ldap",
SAML = "saml" SAML = "saml"
} }
+16 -17
View File
@@ -1,7 +1,6 @@
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; 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 { 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 { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; 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 { AuthMethod } from "../auth/auth-type";
import { TUserDALFactory } from "./user-dal"; import { TUserDALFactory } from "./user-dal";
// TODO: Pick all of these
type TUserServiceFactoryDep = { type TUserServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: Pick<
userAliasDAL: TUserAliasDALFactory; TUserDALFactory,
orgDAL: TOrgDALFactory; | "find"
orgMembershipDAL: TOrgMembershipDALFactory; | "findOne"
tokenService: TAuthTokenServiceFactory; | "findById"
smtpService: TSmtpService; | "transaction"
| "updateById"
| "deleteById"
| "findOneUserAction"
| "createUserAction"
| "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>; export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
@@ -24,13 +32,11 @@ export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ export const userServiceFactory = ({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
// orgDAL,
orgMembershipDAL, orgMembershipDAL,
tokenService, tokenService,
smtpService smtpService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (userId: string) => { const sendEmailVerificationCode = async (userId: string) => {
console.log("sendEmailVerificationCode userId: ", userId);
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email) if (!user.email)
@@ -38,13 +44,11 @@ export const userServiceFactory = ({
if (user.isEmailVerified) if (user.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" });
console.log("sendEmailVerificationCode user: ", user);
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
}); });
console.log("sendEmailVerificationCode 2");
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.EmailVerification, template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
@@ -56,11 +60,6 @@ export const userServiceFactory = ({
}; };
const verifyEmailVerificationCode = async (userId: string, code: string) => { const verifyEmailVerificationCode = async (userId: string, code: string) => {
console.log("verifyEmailVerificationCode args: ", {
userId,
code
});
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (user.isEmailVerified) if (user.isEmailVerified)
+1
View File
@@ -6,6 +6,7 @@ export const publicPaths = [
"/signup", "/signup",
"/signup/sso", "/signup/sso",
"/login", "/login",
"/login/ldap",
"/blog", "/blog",
"/docs", "/docs",
"/changelog", "/changelog",
+4 -2
View File
@@ -3,7 +3,8 @@ export {
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useMergeUsers, useMergeUsers,
useSendEmailVerificationCode, useSendEmailVerificationCode,
useVerifyEmailVerificationCode} from "./mutation"; useVerifyEmailVerificationCode
} from "./mutation";
export { export {
fetchOrgUsers, fetchOrgUsers,
fetchUsersWithMyEmail, fetchUsersWithMyEmail,
@@ -26,4 +27,5 @@ export {
useRevokeMySessions, useRevokeMySessions,
useUpdateMfaEnabled, useUpdateMfaEnabled,
useUpdateOrgUserRole, useUpdateOrgUserRole,
useUpdateUserAuthMethods} from "./queries"; useUpdateUserAuthMethods
} from "./queries";
+23 -1
View File
@@ -5,6 +5,7 @@ import {
encryptAssymmetric encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./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 = () => { export const useSendEmailVerificationCode = () => {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
await apiRequest.post("/api/v2/users/me/emails/code"); await sendEmailVerificationCode();
return {}; return {};
} }
}); });
@@ -88,12 +93,29 @@ export const useVerifyEmailVerificationCode = () => {
}; };
export const useMergeUsers = () => { export const useMergeUsers = () => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ username }: { username: string }) => { mutationFn: async ({ username }: { username: string }) => {
const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", { const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", {
username username
}); });
return data; 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();
} }
}); });
}; };
+5
View File
@@ -27,6 +27,11 @@ export type User = {
id: string; id: string;
}; };
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
}
export type UserEnc = { export type UserEnc = {
encryptionVersion?: number; encryptionVersion?: number;
protectedKey?: string; protectedKey?: string;
-1
View File
@@ -7,7 +7,6 @@ import { Login } from "@app/views/Login";
export default function LoginPage() { export default function LoginPage() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6"> <div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
<Head> <Head>
+27
View File
@@ -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 (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<LoginLDAP />
</div>
);
}
+1 -3
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery"; import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, LDAPStep, MFAStep, SAMLSSOStep } from "./components"; import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils"; import { navigateUserToSelectOrg } from "./Login.utils";
export const Login = () => { export const Login = () => {
@@ -58,8 +58,6 @@ export const Login = () => {
); );
case 2: case 2:
return <SAMLSSOStep setStep={setStep} />; return <SAMLSSOStep setStep={setStep} />;
case 3:
return <LDAPStep setStep={setStep} />;
default: default:
return <div />; return <div />;
} }
@@ -1,24 +1,23 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries"; import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = { export const LoginLDAP = () => {
setStep: (step: number) => void; 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(passedOrgSlug || "");
const [username, setUsername] = useState(passedUsername || "");
const [organizationSlug, setOrganizationSlug] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const { t } = useTranslation(); const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e: React.FormEvent) => { const handleSubmission = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
@@ -42,7 +41,6 @@ export const LDAPStep = ({ setStep }: Props) => {
type: "success" type: "success"
}); });
// redirects either to /login/sso or /signup/sso
window.open(nextUrl); window.open(nextUrl);
window.close(); window.close();
} catch (err) { } catch (err) {
@@ -76,6 +74,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email" autoComplete="email"
id="email" id="email"
className="h-12" className="h-12"
isDisabled={passedOrgSlug !== null}
/> />
</div> </div>
</div> </div>
@@ -90,6 +89,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email" autoComplete="email"
id="email" id="email"
className="h-12" className="h-12"
isDisabled={passedUsername !== null}
/> />
</div> </div>
</div> </div>
@@ -122,7 +122,7 @@ export const LDAPStep = ({ setStep }: Props) => {
<div className="mt-4 flex flex-row items-center justify-center"> <div className="mt-4 flex flex-row items-center justify-center">
<button <button
onClick={() => { onClick={() => {
setStep(0); router.push("/login");
}} }}
type="button" type="button"
className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4" className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
@@ -25,7 +25,7 @@ type Props = {
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => { export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false); const [loginError, setLoginError] = useState(false);
@@ -33,7 +33,10 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
useEffect(() => { useEffect(() => {
if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") { if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
window.open( window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${ `/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
@@ -42,7 +45,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
); );
window.close(); window.close();
} }
}, []) }, []);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => { const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@@ -196,7 +199,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
setStep(3); router.push("/login/ldap");
}} }}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full" className="mx-0 h-10 w-full"
@@ -1 +0,0 @@
export { LDAPStep } from "./LDAPStep";
@@ -1,5 +1,4 @@
export { InitialStep } from "./InitialStep"; export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep"; export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep"; export { SAMLSSOStep } from "./SAMLSSOStep";
+1
View File
@@ -1,2 +1,3 @@
export { Login } from "./Login"; export { Login } from "./Login";
export { LoginLDAP } from "./LoginLDAP";
export { LoginSSO } from "./LoginSSO"; export { LoginSSO } from "./LoginSSO";
@@ -49,7 +49,6 @@ type Props = {
}; };
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => { export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { user } = useUser(); const { user } = useUser();
@@ -150,6 +149,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
[members, searchMemberFilter] [members, searchMemberFilter]
); );
console.log("filterdUser: ", filterdUser);
return ( return (
<div> <div>
<Input <Input
@@ -218,7 +219,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
variant="outline_bg" variant="outline_bg"
onClick={() => onResendInvite(email)} onClick={() => onResendInvite(email)}
> >
Resend Invite Resend invite
</Button> </Button>
)} )}
</> </>
+11 -5
View File
@@ -5,7 +5,8 @@ import {
BackupPDFStep, BackupPDFStep,
EmailConfirmationStep, EmailConfirmationStep,
MergeUsersStep, MergeUsersStep,
UserInfoSSOStep} from "./components"; UserInfoSSOStep
} from "./components";
type Props = { type Props = {
providerAuthToken: string; providerAuthToken: string;
@@ -15,9 +16,8 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const { username, email, organizationName, firstName, lastName } = jwt_decode( const { username, email, organizationName, organizationSlug, firstName, lastName, authType } =
providerAuthToken jwt_decode(providerAuthToken) as any;
) as any;
const renderView = () => { const renderView = () => {
switch (step) { switch (step) {
@@ -37,7 +37,13 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
case 1: case 1:
return <EmailConfirmationStep email={email} setStep={setStep} />; return <EmailConfirmationStep email={email} setStep={setStep} />;
case 2: case 2:
return <MergeUsersStep username={username} />; return (
<MergeUsersStep
username={username}
authType={authType}
organizationSlug={organizationSlug}
/>
);
case 3: case 3:
return ( return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} /> <BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
@@ -6,11 +6,11 @@ import ReactCodeInput from "react-code-input";
import Error from "@app/components/basic/Error"; import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { useUser } from "@app/context";
import { import {
fetchUsersWithMyEmail, fetchUsersWithMyEmail,
useSendEmailVerificationCode, useSendEmailVerificationCode,
useVerifyEmailVerificationCode} from "@app/hooks/api"; useVerifyEmailVerificationCode
} from "@app/hooks/api";
type Props = { type Props = {
email: string; email: string;
@@ -56,7 +56,6 @@ const propsPhone = {
} as const; } as const;
export const EmailConfirmationStep = ({ email, setStep }: Props) => { export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const { user } = useUser();
const [code, setCode] = useState(""); const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false); const [isResendingVerificationEmail] = useState(false);
@@ -106,9 +105,6 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
<p className="text-md flex justify-center text-bunker-200"> <p className="text-md flex justify-center text-bunker-200">
We&apos;ve sent a verification code to {email} We&apos;ve sent a verification code to {email}
</p> </p>
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">
{user?.email}
</p>
<div className="mx-auto hidden w-max min-w-[20rem] md:block"> <div className="mx-auto hidden w-max min-w-[20rem] md:block">
<ReactCodeInput <ReactCodeInput
name="" name=""
@@ -1,3 +1,4 @@
import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { faUsers } from "@fortawesome/free-solid-svg-icons";
@@ -5,6 +6,8 @@ import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
EmptyState, EmptyState,
Modal,
ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -15,28 +18,49 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api"; import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
type Props = { type Props = {
username: string; username: string;
authType?: UserAliasType;
organizationSlug: string;
}; };
export const MergeUsersStep = ({ username }: Props) => { export const MergeUsersStep = ({ username, authType, organizationSlug }: Props) => {
const router = useRouter(); const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [targetUsername, setTargetUsername] = useState("");
const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail(); const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail();
const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers(); const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers();
const handleMergeUser = async (targetUsername: string) => { const handleMergeUser = async (mergeWithUsername: string) => {
try { try {
console.log("merge A"); if (!mergeWithUsername) return;
await mergeUser({ username: targetUsername }); await mergeUser({ username: mergeWithUsername });
// TODO: logout, make user re-login
console.log("merge B");
createNotification({ createNotification({
text: "Successfully merged user", text: "Successfully merged user",
type: "success" 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) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
@@ -72,14 +96,16 @@ export const MergeUsersStep = ({ username }: Props) => {
return ( return (
<Tr className="h-10 items-center" key={`same-email-user-${user.id}`}> <Tr className="h-10 items-center" key={`same-email-user-${user.id}`}>
<Td>{`${user.firstName ?? ""} ${user.lastName ?? ""}`}</Td> <Td>{`${user.firstName ?? ""} ${user.lastName ?? ""}`}</Td>
<Td>{username}</Td> <Td>{user.username}</Td>
<Td> <Td>
<Button <Button
isLoading={isLoadingMerge}
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
onClick={() => handleMergeUser(user.username)} onClick={() => {
setIsOpen(true);
setTargetUsername(user.username);
}}
> >
Merge Merge
</Button> </Button>
@@ -97,6 +123,35 @@ export const MergeUsersStep = ({ username }: Props) => {
</TBody> </TBody>
</Table> </Table>
</TableContainer> </TableContainer>
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<ModalContent title="Merge User Confirmation">
<p className="mb-4 text-bunker-300">
The merge operation will transfer / consolidate your existing organization membership to
the target user you&apos;re merging with.
</p>
<p className="mb-4 text-bunker-300">
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.
</p>
<p className="text-bunker-300">
Once the merge operation is complete, you&apos;ll be prompted to re-login.
</p>
<div className="mt-8 flex items-center">
<Button
isLoading={isLoadingMerge}
colorSchema="primary"
onClick={async () => handleMergeUser(targetUsername)}
className="mr-4"
>
Confirm
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => setIsOpen(false)}>
Cancel
</Button>
</div>
</ModalContent>
</Modal>
</div> </div>
); );
}; };
@@ -17,6 +17,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { sendEmailVerificationCode } from "@app/hooks/api/users/mutation";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@@ -205,10 +206,11 @@ export const UserInfoSSOStep = ({
if (email) { if (email) {
// move to verify email // move to verify email
await sendEmailVerificationCode();
setStep(1); setStep(1);
} else { } else {
// move to backup PDF step // move to backup PDF step
setStep(2); setStep(3);
} }
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);