mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Finish preliminary email validation, merge user flow w saml/ldap
This commit is contained in:
@@ -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> {}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
orgMembershipDAL: TOrgMembershipDALFactory; // TODO: Pick
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -46,7 +46,6 @@ export type TGetSamlCfgDTO =
|
||||
|
||||
export type TSamlLoginDTO = {
|
||||
externalId: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({
|
||||
superAdmin: true,
|
||||
isGhost: false,
|
||||
isAccepted: true,
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum UserAliasType {
|
||||
export enum TUserAliasType {
|
||||
LDAP = "ldap",
|
||||
SAML = "saml"
|
||||
}
|
||||
|
||||
@@ -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<TUserAliasDALFactory, "find" | "insertMany">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||
@@ -24,13 +32,11 @@ export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||
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)
|
||||
|
||||
@@ -6,6 +6,7 @@ export const publicPaths = [
|
||||
"/signup",
|
||||
"/signup/sso",
|
||||
"/login",
|
||||
"/login/ldap",
|
||||
"/blog",
|
||||
"/docs",
|
||||
"/changelog",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -27,6 +27,11 @@ export type User = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export enum UserAliasType {
|
||||
LDAP = "ldap",
|
||||
SAML = "saml"
|
||||
}
|
||||
|
||||
export type UserEnc = {
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Login } from "@app/views/Login";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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">
|
||||
<Head>
|
||||
|
||||
27
frontend/src/pages/login/ldap/index.tsx
Normal file
27
frontend/src/pages/login/ldap/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 <SAMLSSOStep setStep={setStep} />;
|
||||
case 3:
|
||||
return <LDAPStep setStep={setStep} />;
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,6 +89,7 @@ export const LDAPStep = ({ setStep }: Props) => {
|
||||
autoComplete="email"
|
||||
id="email"
|
||||
className="h-12"
|
||||
isDisabled={passedUsername !== null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ export const LDAPStep = ({ setStep }: Props) => {
|
||||
<div className="mt-4 flex flex-row items-center justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
router.push("/login");
|
||||
}}
|
||||
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"
|
||||
@@ -25,7 +25,7 @@ type Props = {
|
||||
|
||||
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = 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);
|
||||
|
||||
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");
|
||||
window.open(
|
||||
`/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();
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -196,7 +199,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
setStep(3);
|
||||
router.push("/login/ldap");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||
className="mx-0 h-10 w-full"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { LDAPStep } from "./LDAPStep";
|
||||
@@ -1,5 +1,4 @@
|
||||
export { InitialStep } from "./InitialStep";
|
||||
export { LDAPStep } from "./LDAPStep";
|
||||
export { MFAStep } from "./MFAStep";
|
||||
export { SAMLSSOStep } from "./SAMLSSOStep";
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { Login } from "./Login";
|
||||
export { LoginLDAP } from "./LoginLDAP";
|
||||
export { LoginSSO } from "./LoginSSO";
|
||||
|
||||
@@ -49,7 +49,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { user } = useUser();
|
||||
@@ -150,6 +149,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
|
||||
[members, searchMemberFilter]
|
||||
);
|
||||
|
||||
console.log("filterdUser: ", filterdUser);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
@@ -218,7 +219,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
|
||||
variant="outline_bg"
|
||||
onClick={() => onResendInvite(email)}
|
||||
>
|
||||
Resend Invite
|
||||
Resend invite
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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 <EmailConfirmationStep email={email} setStep={setStep} />;
|
||||
case 2:
|
||||
return <MergeUsersStep username={username} />;
|
||||
return (
|
||||
<MergeUsersStep
|
||||
username={username}
|
||||
authType={authType}
|
||||
organizationSlug={organizationSlug}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<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 { 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) => {
|
||||
<p className="text-md flex justify-center text-bunker-200">
|
||||
We've sent a verification code to {email}
|
||||
</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">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@@ -5,6 +6,8 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -15,28 +18,49 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api";
|
||||
import { UserAliasType } from "@app/hooks/api/users/types";
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
authType?: UserAliasType;
|
||||
organizationSlug: string;
|
||||
};
|
||||
|
||||
export const MergeUsersStep = ({ username }: Props) => {
|
||||
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 (
|
||||
<Tr className="h-10 items-center" key={`same-email-user-${user.id}`}>
|
||||
<Td>{`${user.firstName ?? ""} ${user.lastName ?? ""}`}</Td>
|
||||
<Td>{username}</Td>
|
||||
<Td>{user.username}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
isLoading={isLoadingMerge}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handleMergeUser(user.username)}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
setTargetUsername(user.username);
|
||||
}}
|
||||
>
|
||||
Merge
|
||||
</Button>
|
||||
@@ -97,6 +123,35 @@ export const MergeUsersStep = ({ username }: Props) => {
|
||||
</TBody>
|
||||
</Table>
|
||||
</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'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'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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user