Merge branch 'Infisical:main' into main

This commit is contained in:
Aashish Upadhyay
2023-03-17 15:48:12 +05:45
committed by GitHub
21 changed files with 394 additions and 182 deletions
+1
View File
@@ -48,3 +48,4 @@ export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
@@ -7,7 +7,7 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -99,7 +99,7 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg; let invitee, inviteeMembershipOrg, completeInviteLink;
try { try {
const { organizationId, inviteeEmail } = req.body; const { organizationId, inviteeEmail } = req.body;
@@ -181,6 +181,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
callback_url: getSiteURL() + '/signupinvite' callback_url: getSiteURL() + '/signupinvite'
} }
}); });
if (!getSmtpConfigured()) {
completeInviteLink = `${getSiteURL() + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
}
} }
await updateSubscriptionOrgQuantity({ organizationId }); await updateSubscriptionOrgQuantity({ organizationId });
@@ -193,7 +197,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}` message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
}); });
}; };
@@ -7,7 +7,7 @@ import {
} from '../../helpers/signup'; } from '../../helpers/signup';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
try { try {
email = req.body.email; email = req.body.email;
if (getInviteOnlySignup() || false) { if (getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account // Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({}) const userCount = await User.countDocuments({})
if (userCount != 0) { if (userCount != 0) {
@@ -66,7 +66,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
const { email, code } = req.body; const { email, code } = req.body;
// initialize user account // initialize user account
user = await User.findOne({ email }); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
@@ -75,10 +75,12 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
} }
// verify email // verify email
if (getSmtpConfigured()) {
await checkEmailVerification({ await checkEmailVerification({
email, email,
code code
}); });
}
if (!user) { if (!user) {
user = await new User({ user = await new User({
@@ -317,6 +317,18 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
tag: string; tag: string;
} }
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const resObj: VerifyMfaTokenRes = { const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion, encryptionVersion: user.encryptionVersion,
token: tokens.token, token: tokens.token,
+3 -1
View File
@@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import handlebars from 'handlebars'; import handlebars from 'handlebars';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { getSmtpFromName, getSmtpFromAddress } from '../config'; import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config';
let smtpTransporter: nodemailer.Transporter; let smtpTransporter: nodemailer.Transporter;
@@ -25,6 +25,7 @@ const sendMail = async ({
recipients: string[]; recipients: string[];
substitutions: any; substitutions: any;
}) => { }) => {
if (getSmtpConfigured()) {
try { try {
const html = fs.readFileSync( const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template), path.resolve(__dirname, '../templates/' + template),
@@ -43,6 +44,7 @@ const sendMail = async ({
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
} }
}
}; };
const setTransporter = (transporter: nodemailer.Transporter) => { const setTransporter = (transporter: nodemailer.Transporter) => {
+2
View File
@@ -73,9 +73,11 @@ import {
} from './config'; } from './config';
const main = async () => { const main = async () => {
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
await infisical.connect({ await infisical.connect({
token: process.env.INFISICAL_TOKEN! token: process.env.INFISICAL_TOKEN!
}); });
}
logTelemetryMessage(); logTelemetryMessage();
setTransporter(initSmtp()); setTransporter(initSmtp());
+2
View File
@@ -1,4 +1,5 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import { getSmtpConfigured } from '../../config';
const router = express.Router(); const router = express.Router();
@@ -8,6 +9,7 @@ router.get(
res.status(200).json({ res.status(200).json({
date: new Date(), date: new Date(),
message: 'Ok', message: 'Ok',
emailConfigured: getSmtpConfigured()
}) })
} }
); );
@@ -2,10 +2,13 @@ import React, { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { usePopUp } from '@app/hooks/usePopUp';
import addUserToOrg from '@app/pages/api/organization/addUserToOrg'; import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces'; import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import { EmailServiceSetupModal } from '../v2';
/** /**
* This is the last step of the signup flow. People can optionally invite their teammates here. * This is the last step of the signup flow. People can optionally invite their teammates here.
@@ -14,6 +17,10 @@ export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState(''); const [emails, setEmails] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
// Redirect user to the getting started page // Redirect user to the getting started page
const redirectToHome = async () => { const redirectToHome = async () => {
@@ -62,10 +69,20 @@ export default function TeamInviteStep(): JSX.Element {
</div> </div>
<Button <Button
text={t('signup:step5-send-invites') ?? ''} text={t('signup:step5-send-invites') ?? ''}
onButtonPressed={() => inviteUsers({ emails })} onButtonPressed={() => {
if(serverDetails?.emailConfigured){
inviteUsers({ emails })
}else{
handlePopUpOpen('setUpEmail');
}
}}
size="lg" size="lg"
/> />
</div> </div>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
} }
@@ -0,0 +1,21 @@
import { Button } from '../Button';
import { Modal, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
};
export const EmailServiceSetupModal = ({ isOpen, onOpenChange }: Props): JSX.Element => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Email service not configured">
<p className="mb-4 text-bunker-300">
The administrators of this Infisical instance have not yet set up an email service provider required to perform this action
</p>
<a href="https://infisical.com/docs/self-hosting/configuration/email">
<Button className="mr-4">Learn more</Button>
</a>
</ModalContent>
</Modal>
);
@@ -0,0 +1 @@
export { EmailServiceSetupModal } from './EmailServiceSetupModal';
+1
View File
@@ -3,6 +3,7 @@ export * from './Card';
export * from './Checkbox'; export * from './Checkbox';
export * from './DeleteActionModal'; export * from './DeleteActionModal';
export * from './Dropdown'; export * from './Dropdown';
export * from './EmailServiceSetupModal'
export * from './EmptyState'; export * from './EmptyState';
export * from './FormControl'; export * from './FormControl';
export * from './IconButton'; export * from './IconButton';
@@ -0,0 +1 @@
export { useFetchServerStatus } from './queries'
@@ -0,0 +1,19 @@
import {useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { ServerStatus } from './types';
// cache key
const serverStatusKeys = {
serverStatus: ['serverStatus'] as const
};
const fetchServerStatus = async () => {
const {data} = await apiRequest.get<ServerStatus>('/api/status');
return data;
};
export const useFetchServerStatus= () => {
return useQuery({ queryKey: serverStatusKeys.serverStatus, queryFn: fetchServerStatus });
}
@@ -0,0 +1,5 @@
export type ServerStatus = {
date: string;
message: string;
emailConfigured: boolean;
};
+7 -1
View File
@@ -83,8 +83,14 @@ export const useAddUserToWs = () => {
export const useAddUserToOrg = () => { export const useAddUserToOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
type Response = {
data: {
message: string,
completeInviteLink: string | undefined
}
}
return useMutation<{}, {}, AddUserToOrgDTO>({ return useMutation<Response, {}, AddUserToOrgDTO>({
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto), mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
onSuccess: (_, { organizationId }) => { onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+18 -4
View File
@@ -13,6 +13,7 @@ import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep'; import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
import getWorkspaces from './api/workspace/getWorkspaces'; import getWorkspaces from './api/workspace/getWorkspaces';
@@ -25,10 +26,12 @@ export default function SignUp() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const [code, setCode] = useState(''); const [code, setCode] = useState('123456');
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const router = useRouter(); const router = useRouter();
const {data: serverDetails } = useFetchServerStatus()
const { t } = useTranslation(); const { t } = useTranslation();
@@ -68,6 +71,19 @@ export default function SignUp() {
} }
}; };
// when email service is not configured, skip step 2 and 5
useEffect(() => {
if (!serverDetails?.emailConfigured && step === 2){
incrementStep()
}
if (!serverDetails?.emailConfigured && step === 5){
getWorkspaces().then((userWorkspaces)=>{
router.push(`/dashboard/${userWorkspaces[0]._id}`);
});
}
}, [step]);
return ( return (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center"> <div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<Head> <Head>
@@ -111,9 +127,7 @@ export default function SignUp() {
password={password} password={password}
name={`${firstName} ${lastName}`} name={`${firstName} ${lastName}`}
/> />
) : ( ) : (serverDetails?.emailConfigured ? <TeamInviteStep /> : "")}
<TeamInviteStep />
)}
</form> </form>
</div> </div>
</div> </div>
+19 -1
View File
@@ -6,12 +6,19 @@ import Link from 'next/link';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField'; import InputField from '@app/components/basic/InputField';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { EmailServiceSetupModal } from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset'; import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset';
export default function VerifyEmail() { export default function VerifyEmail() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
/** /**
* This function sends the verification email and forwards a user to the next step. * This function sends the verification email and forwards a user to the next step.
@@ -63,7 +70,13 @@ export default function VerifyEmail() {
</div> </div>
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm"> <div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg"> <div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button text="Continue" onButtonPressed={sendVerificationEmail} size="lg" /> <Button text="Continue" onButtonPressed={()=>{
if (serverDetails?.emailConfigured){
sendVerificationEmail()
} else {
handlePopUpOpen('setUpEmail');
}
}} size="lg" />
</div> </div>
</div> </div>
</div> </div>
@@ -80,6 +93,11 @@ export default function VerifyEmail() {
</div> </div>
</div> </div>
)} )}
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
} }
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { plans } from 'public/data/frequentConstants'; import { plans } from 'public/data/frequentConstants';
@@ -51,6 +52,8 @@ export const OrgSettingsPage = () => {
const addIncidentContact = useAddIncidentContact(); const addIncidentContact = useAddIncidentContact();
const removeIncidentContact = useDeleteIncidentContact(); const removeIncidentContact = useDeleteIncidentContact();
const [completeInviteLink, setcompleteInviteLink] = useState<string|undefined>("")
const isMoreUsersNotAllowed = const isMoreUsersNotAllowed =
(orgUsers || []).length >= 5 && (orgUsers || []).length >= 5 &&
subscriptionPlan === plans.starter && subscriptionPlan === plans.starter &&
@@ -96,11 +99,16 @@ export const OrgSettingsPage = () => {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
try { try {
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email }); const {data} = await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
setcompleteInviteLink(data?.completeInviteLink)
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink){
createNotification({ createNotification({
text: 'Successfully invited user to the organization.', text: 'Successfully invited user to the organization.',
type: 'success' type: 'success'
}); });
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
@@ -247,6 +255,8 @@ export const OrgSettingsPage = () => {
onRemoveMember={onRemoveUserOrgMembership} onRemoveMember={onRemoveUserOrgMembership}
onRoleChange={onUpdateOrgUserRole} onRoleChange={onUpdateOrgUserRole}
onGrantAccess={onGrantUserAccess} onGrantAccess={onGrantUserAccess}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setcompleteInviteLink}
/> />
</div> </div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6"> <div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
@@ -13,6 +13,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmailServiceSetupModal,
EmptyState, EmptyState,
FormControl, FormControl,
IconButton, IconButton,
@@ -28,6 +29,7 @@ import {
THead, THead,
Tr} from '@app/components/v2'; Tr} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { IncidentContact } from '@app/hooks/api/types'; import { IncidentContact } from '@app/hooks/api/types';
type Props = { type Props = {
@@ -50,9 +52,11 @@ export const OrgIncidentContactsTable = ({
isLoading isLoading
}: Props) => { }: Props) => {
const [searchContact, setSearchContact] = useState(''); const [searchContact, setSearchContact] = useState('');
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addContact', 'addContact',
'removeContact' 'removeContact',
'setUpEmail'
] as const); ] as const);
const { const {
@@ -92,7 +96,13 @@ export const OrgIncidentContactsTable = ({
<div> <div>
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen('addContact')} onClick={() => {
if (serverDetails?.emailConfigured){
handlePopUpOpen('addContact');
} else {
handlePopUpOpen('setUpEmail');
}
}}
> >
Add Contact Add Contact
</Button> </Button>
@@ -180,6 +190,10 @@ export const OrgIncidentContactsTable = ({
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)} onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
onDeleteApproved={onRemoveIncidentContact} onDeleteApproved={onRemoveIncidentContact}
/> />
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
}; };
@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'; import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -9,7 +9,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState, EmailServiceSetupModal, EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -28,6 +28,7 @@ import {
Tr, Tr,
UpgradePlanModal} from '@app/components/v2'; UpgradePlanModal} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { OrgUser, Workspace } from '@app/hooks/api/types'; import { OrgUser, Workspace } from '@app/hooks/api/types';
type Props = { type Props = {
@@ -42,6 +43,8 @@ type Props = {
onGrantAccess: (userId: string, publicKey: string) => Promise<void>; onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
// the current user id to block remove org button // the current user id to block remove org button
userId: string; userId: string;
completeInviteLink: string | undefined,
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
}; };
const addMemberFormSchema = yup.object({ const addMemberFormSchema = yup.object({
@@ -60,14 +63,18 @@ export const OrgMembersTable = ({
onGrantAccess, onGrantAccess,
onRoleChange, onRoleChange,
userId, userId,
isLoading isLoading,
completeInviteLink,
setCompleteInviteLink
}: Props) => { }: Props) => {
const router = useRouter(); const router = useRouter();
const [searchMemberFilter, setSearchMemberFilter] = useState(''); const [searchMemberFilter, setSearchMemberFilter] = useState('');
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addMember', 'addMember',
'removeMember', 'removeMember',
'upgradePlan' 'upgradePlan',
'setUpEmail'
] as const); ] as const);
const { const {
@@ -79,7 +86,10 @@ export const OrgMembersTable = ({
const onAddMember = async ({ email }: TAddMemberForm) => { const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email); await onInviteMember(email);
if (serverDetails?.emailConfigured){
handlePopUpClose('addMember'); handlePopUpClose('addMember');
}
reset(); reset();
}; };
@@ -106,6 +116,11 @@ export const OrgMembersTable = ({
[members, searchMemberFilter] [members, searchMemberFilter]
); );
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
// setIsTokenCopied.on();
};
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex"> <div className="mb-4 flex">
@@ -121,11 +136,16 @@ export const OrgMembersTable = ({
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
// if (serverDetails?.emailConfigured){
if (isMoreUserNotAllowed) { if (isMoreUserNotAllowed) {
handlePopUpOpen('upgradePlan'); handlePopUpOpen('upgradePlan');
} else { } else {
reset();
handlePopUpOpen('addMember'); handlePopUpOpen('addMember');
} }
// } else {
// handlePopUpOpen('setUpEmail');
// }
}} }}
> >
Add Member Add Member
@@ -173,7 +193,7 @@ export const OrgMembersTable = ({
<SelectItem value="member">member</SelectItem> <SelectItem value="member">member</SelectItem>
</Select> </Select>
)} )}
{(status === 'invited' || status === 'verified') && ( {((status === 'invited' || status === 'verified') && serverDetails?.emailConfigured) && (
<Button className='w-40' colorSchema="secondary" onClick={() => onInviteMember(email)}> <Button className='w-40' colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resend Invite Resend Invite
</Button> </Button>
@@ -234,20 +254,23 @@ export const OrgMembersTable = ({
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle('addMember', isOpen); handlePopUpToggle('addMember', isOpen);
reset(); setCompleteInviteLink(undefined)
}} }}
> >
<ModalContent <ModalContent
title={`Invite others to ${orgName}`} title={`Invite others to ${orgName}`}
subTitle={ subTitle={
<> <div>
{!completeInviteLink && <div>
An invite is specific to an email address and expires after 1 day. An invite is specific to an email address and expires after 1 day.
<br /> <br />
For security reasons, you will need to separately add members to projects. For security reasons, you will need to separately add members to projects.
</> </div>}
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
} }
> >
<form onSubmit={handleSubmit(onAddMember)}> {!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -276,7 +299,22 @@ export const OrgMembersTable = ({
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>}
{
completeInviteLink &&
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={false ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
</IconButton>
</div>
}
</ModalContent> </ModalContent>
</Modal> </Modal>
<DeleteActionModal <DeleteActionModal
@@ -291,6 +329,10 @@ export const OrgMembersTable = ({
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan." text="You can add custom environments if you switch to Infisical's Team plan."
/> />
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
}; };
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { Checkbox } from '@app/components/v2'; import { Checkbox, EmailServiceSetupModal } from '@app/components/v2';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { usePopUp } from '@app/hooks/usePopUp';
import { useGetUser } from '../../../../hooks/api'; import { useGetUser } from '../../../../hooks/api';
import { User } from '../../../../hooks/api/types'; import { User } from '../../../../hooks/api/types';
@@ -11,6 +13,11 @@ export const SecuritySection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false); const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser(); const { data: user } = useGetUser();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
const {data: serverDetails } = useFetchServerStatus()
useEffect(() => { useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') { if (user && typeof user.isMfaEnabled !== 'undefined') {
@@ -42,6 +49,7 @@ export const SecuritySection = () => {
} }
return ( return (
<>
<form> <form>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2"> <div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold"> <p className="mb-4 mt-2 text-xl font-semibold">
@@ -52,12 +60,21 @@ export const SecuritySection = () => {
id="isTwoFAEnabled" id="isTwoFAEnabled"
isChecked={isMfaEnabled} isChecked={isMfaEnabled}
onCheckedChange={(state) => { onCheckedChange={(state) => {
if (serverDetails?.emailConfigured){
toggleMfa(state as boolean); toggleMfa(state as boolean);
} else {
handlePopUpOpen('setUpEmail');
}
}} }}
> >
Enable 2-factor authentication via your personal email. Enable 2-factor authentication via your personal email.
</Checkbox> </Checkbox>
</div> </div>
</form> </form>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</>
); );
}; };