Update package.json

This commit is contained in:
Tuan Dang
2023-02-11 16:07:32 +07:00
309 changed files with 13900 additions and 2982 deletions
+34 -10
View File
@@ -4,12 +4,20 @@ import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { appWithTranslation } from 'next-i18next';
import { config } from '@fortawesome/fontawesome-svg-core';
import { QueryClientProvider } from '@tanstack/react-query';
import Layout from '@app/components/basic/Layout';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import RouteGuard from '@app/components/RouteGuard';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
import { publicPaths } from '@app/const';
import {
AuthProvider,
OrgProvider,
SubscriptionProvider,
UserProvider,
WorkspaceProvider
} from '@app/context';
import { AppLayout } from '@app/layouts';
import { queryClient } from '@app/reactQuery';
import '@fortawesome/fontawesome-svg-core/styles.css';
import '../styles/globals.css';
@@ -54,17 +62,33 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
!Component.requireAuth
) {
return <Component {...pageProps} />;
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</QueryClientProvider>
);
}
return (
<RouteGuard>
<NotificationProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</NotificationProvider>
</RouteGuard>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<WorkspaceProvider>
<OrgProvider>
<SubscriptionProvider>
<UserProvider>
<NotificationProvider>
<AppLayout>
<Component {...pageProps} />
</AppLayout>
</NotificationProvider>
</UserProvider>
</SubscriptionProvider>
</OrgProvider>
</WorkspaceProvider>
</AuthProvider>
</QueryClientProvider>
);
};
+1 -1
View File
@@ -124,7 +124,7 @@ export default function Activity() {
};
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<div className="mx-6 lg:mx-0 w-full h-screen">
<NavHeader pageName="Project Activity" isProjectRelated />
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
@@ -1,7 +1,15 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
integrationAuthId: string;
integrationAuthId: string;
isActive: boolean;
app: string | null;
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
owner: string | null;
path: string | null;
region: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +18,15 @@ interface Props {
* @returns
*/
const createIntegration = ({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner,
path,
region
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +34,15 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner,
path,
region
})
}).then(async (res) => {
if (res && res.status === 200) {
@@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
}
/**
@@ -19,6 +20,7 @@ interface Props {
const saveIntegrationAccessToken = ({
workspaceId,
integration,
accessId,
accessToken
}: Props) =>
SecurityClient.fetchCall(`/api/v1/integration-auth/access-token`, {
@@ -29,13 +31,14 @@ const saveIntegrationAccessToken = ({
body: JSON.stringify({
workspaceId,
integration,
accessId,
accessToken
})
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).integrationAuth;
}
console.log('Failed to save integration access token');
console.log('Failed to save integration access details');
return undefined;
});
@@ -13,6 +13,7 @@ export interface IMembershipOrg {
organization: string;
role: 'owner' | 'admin' | 'member';
status: 'invited' | 'accepted';
deniedPermissions: any[];
}
/**
* This route lets us get all the users in an org.
@@ -0,0 +1,27 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This function change the access of a user in a certain organization
* @param {string} organizationId
* @param {string} membershipId
* @param {string} role
* @returns
*/
const changeUserRoleInOrganization = (organizationId: string, membershipId: string, role: string) =>
SecurityClient.fetchCall(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
role
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log('Failed to change the user role in an org');
return undefined;
});
export default changeUserRoleInOrganization;
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the environments that a certain user has acess to in a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceEnvironments = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).accessibleEnvironments;
}
console.log('Failed to get accessible environments');
return undefined;
});
export default getWorkspaceEnvironments;
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the tags for a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspaceTags;
}
console.log('Failed to get the tags available in a certain project');
return undefined;
});
export default getWorkspaceTags;
@@ -4,6 +4,7 @@ interface Workspace {
__v: number;
_id: string;
name: string;
autoCapitalization: boolean;
organization: string;
environments: Array<{ name: string; slug: string }>;
}
+258 -90
View File
@@ -5,23 +5,26 @@ import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
faArrowDownAZ,
faArrowDownZA,
faArrowDown,
faArrowLeft,
faArrowUp,
faCheck,
faClockRotateLeft,
faEye,
faEyeSlash,
faFolderOpen,
faMagnifyingGlass,
faPlus
faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal';
import DownloadSecretMenu from '@app/components/dashboard/DownloadSecretsMenu';
import DropZone from '@app/components/dashboard/DropZone';
import KeyPair from '@app/components/dashboard/KeyPair';
@@ -31,6 +34,7 @@ import guidGenerator from '@app/components/utilities/randomId';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { IconButton } from '@app/components/v2';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
@@ -41,11 +45,14 @@ import updateSecrets from '../api/files/UpdateSecrets';
import getUser from '../api/user/getUser';
import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = {
name: string;
slug: string;
isWriteDenied: boolean;
};
interface SecretDataProps {
@@ -56,6 +63,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -79,6 +87,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
tags: Tag[];
}[];
}
@@ -111,7 +120,7 @@ export default function Dashboard() {
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
const [isNew, setIsNew] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [searchKeys, setSearchKeys] = useState('');
const [errorDragAndDrop, setErrorDragAndDrop] = useState(false);
const [sortMethod, setSortMethod] = useState('alphabetical');
@@ -123,6 +132,9 @@ export default function Dashboard() {
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const [autoCapitalization, setAutoCapitalization] = useState(false);
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -131,10 +143,7 @@ export default function Dashboard() {
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
name: '',
slug: ''
});
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>();
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
const secretsTop = useRef<HTMLDivElement>(null);
@@ -190,15 +199,18 @@ export default function Dashboard() {
}));
setData(sortedData);
setIsLoading(false);
};
/**
* Reorder rows alphabetically or in the opprosite order
*/
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
if (dataToReorder) {
setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
sortValuesHandler(dataToReorder, undefined);
sortValuesHandler(dataToReorder, undefined);
}
};
useEffect(() => {
@@ -213,10 +225,12 @@ export default function Dashboard() {
if (!workspace) {
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
}
setAutoCapitalization(workspace?.autoCapitalization ?? true);
setWorkspaceEnvs(workspace?.environments || []);
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
setWorkspaceEnvs(accessibleEnvironments || []);
// set env
const env = workspace?.environments?.[0] || {
const env = accessibleEnvironments?.[0] || {
name: 'unknown',
slug: 'unkown'
};
@@ -229,6 +243,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -242,16 +258,17 @@ export default function Dashboard() {
setIsLoading(true);
setBlurred(true);
// ENV
const dataToSort = await getSecretsForProject({
env: selectedEnv.slug,
setIsKeyAvailable,
setData,
workspaceId
});
setInitialData(dataToSort);
reorderRows(dataToSort);
setTimeout(() => setIsLoading(false), 700);
let dataToSort;
if (selectedEnv) {
dataToSort = await getSecretsForProject({
env: selectedEnv.slug,
setIsKeyAvailable,
setData,
workspaceId
});
setInitialData(dataToSort);
reorderRows(dataToSort);
}
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -270,7 +287,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
...data!
]);
@@ -279,6 +297,23 @@ export default function Dashboard() {
}
};
const addRowToBottom = () => {
setIsNew(false);
setData([
...data!,
{
id: guidGenerator(),
idOverride: guidGenerator(),
pos: data!.length,
key: '',
value: '',
valueOverride: undefined,
comment: '',
tags: []
},
]);
};
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true);
toggleSidebar('None');
@@ -312,6 +347,11 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
@@ -329,6 +369,10 @@ export default function Dashboard() {
modifyComment(value, pos);
}, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/**
* Save the changes of environment variables and push them to the database
*/
@@ -351,6 +395,7 @@ export default function Dashboard() {
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (nameErrors) {
setSaveLoading(false);
return createNotification({
text: 'Solve all name errors before saving secrets.',
type: 'error'
@@ -358,12 +403,21 @@ export default function Dashboard() {
}
if (duplicatesExist) {
setSaveLoading(false);
return createNotification({
text: 'Remove duplicated secret names before saving.',
type: 'error'
});
}
if (selectedEnv?.isWriteDenied) {
setSaveLoading(false);
return createNotification({
text: 'You are not allowed to edit this environment',
type: 'error'
});
}
// Once "Save changes" is clicked, disable that button
setButtonReady(false);
@@ -391,7 +445,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment)
initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !==
initDataPoint?.tags
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -425,7 +481,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
@@ -440,7 +497,8 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment)
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !== initDataPoint?.tags)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -452,14 +510,15 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
@@ -467,7 +526,7 @@ export default function Dashboard() {
});
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
}
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
if (selectedEnv && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
@@ -490,11 +549,33 @@ export default function Dashboard() {
return undefined;
};
const addData = (newData: SecretDataProps[]) => {
setData(data!.concat(newData));
const addDataWithMerge = (newData: SecretDataProps[], preserve?: 'old' | 'new') => {
setData((oldData) => {
let filteredOldData = oldData!;
let filteredNewData = newData;
if (preserve === 'new')
filteredOldData = oldData!.filter(
(oldDataPoint) => !newData.find((newDataPoint) => newDataPoint.key === oldDataPoint.key)
);
if (preserve === 'old')
filteredNewData = newData.filter(
(newDataPoint) => !oldData?.find((oldDataPoint) => oldDataPoint.key === newDataPoint.key)
);
return filteredOldData.concat(filteredNewData);
});
setButtonReady(true);
};
const addData = (newData: SecretDataProps[]) => {
if (
newData.some((newDataPoint) => data?.find((dataPoint) => dataPoint.key === newDataPoint.key)) // if newData contains duplicates
) {
setDropZoneData(newData);
return;
}
addDataWithMerge(newData);
};
const changeBlurred = () => {
setBlurred(!blurred);
};
@@ -504,7 +585,7 @@ export default function Dashboard() {
};
return data ? (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head>
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -512,33 +593,23 @@ export default function Dashboard() {
<meta property="og:title" content={String(t('dashboard:og-title'))} />
<meta name="og:description" content={String(t('dashboard:og-description'))} />
</Head>
<div className="flex flex-row">
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className="w-full max-h-96 pb-2">
<div className="flex flex-row h-full">
<ConfirmEnvOverwriteModal
isOpen={!!dropZoneData}
onClose={() => setDropZoneData(undefined)}
onOverwriteConfirm={(preserve) => {
addDataWithMerge(dropZoneData!, preserve);
setDropZoneData(undefined);
}}
duplicateKeys={
dropZoneData
?.filter((newDataPoint) =>
data?.find((dataPoint) => dataPoint.key === newDataPoint.key)
)
.map((duplicate) => duplicate.key) ?? []
}
/>
<div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && (
<BottonRightPopup
@@ -551,7 +622,7 @@ export default function Dashboard() {
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
/>
)}
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl">
{snapshotData && (
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
@@ -572,7 +643,7 @@ export default function Dashboard() {
</span>
)}
</div>
{!snapshotData && data?.length === 0 && (
{!snapshotData && data?.length === 0 && selectedEnv && (
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
@@ -580,7 +651,8 @@ export default function Dashboard() {
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown'
slug: 'unknown',
isWriteDenied: false
}
)
}
@@ -591,7 +663,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -611,7 +686,7 @@ export default function Dashboard() {
/>
</div>
)}
{snapshotData && (
{snapshotData && selectedEnv && (
<div className="flex justify-start max-w-sm mt-1">
<Button
text={String(t('Rollback to this snapshot'))}
@@ -626,7 +701,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride,
key: sv.key,
value: sv.value,
comment: ''
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
@@ -638,6 +714,7 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`,
type: 'success'
});
setButtonReady(false);
}}
color="primary"
size="md"
@@ -648,9 +725,9 @@ export default function Dashboard() {
</div>
</div>
<div className="mx-6 w-full pr-12">
<div className="flex flex-col max-w-5xl pb-1">
<div className="flex flex-col pb-1">
<div className="w-full flex flex-row items-start">
{(snapshotData || data?.length !== 0) && (
{(snapshotData || data?.length !== 0) && selectedEnv && (
<>
{!snapshotData ? (
<ListBox
@@ -660,7 +737,8 @@ export default function Dashboard() {
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown'
slug: 'unknown',
isWriteDenied: false
}
)
}
@@ -673,34 +751,25 @@ export default function Dashboard() {
setSelectedSnapshotEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown'
slug: 'unknown',
isWriteDenied: false
}
)
}
/>
)}
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
className="bg-transparent rounded-l-md py-[0.7rem] pl-4 pr-2 text-bunker-300 text-sm"
icon={faMagnifyingGlass}
/>
<input
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
className="pl-2 text-bunker-300 rounded-r-md bg-transparent w-full h-full outline-none text-sm placeholder:hover:text-bunker-200 placeholder:focus:text-transparent"
value={searchKeys}
onChange={(e) => setSearchKeys(e.target.value)}
placeholder={String(t('dashboard:search-keys'))}
/>
</div>
{!snapshotData && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
onButtonPressed={() => reorderRows(1)}
color="mineshaft"
size="icon-md"
icon={sortMethod === 'alphabetical' ? faArrowDownAZ : faArrowDownZA}
/>
</div>
)}
{!snapshotData && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<DownloadSecretMenu data={data} env={selectedEnv.slug} />
@@ -745,31 +814,88 @@ export default function Dashboard() {
/>
</div>
) : data?.length !== 0 ? (
<div className="flex flex-col w-full mt-1 mb-2">
<div className="flex flex-col w-full mt-1">
<div
onScroll={onSecretsAreaScroll}
className="max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
>
<div ref={secretsTop} />
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
<div
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
>
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div>
</div>
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="none"
onClick={() => {}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
</div>
</div>
</div>
</div>
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData &&
data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
?.filter((row) =>
row.key?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => (
<KeyPair
key={keyPair.id}
isCapitalized={autoCapitalization}
key={keyPair.id ? keyPair.id : keyPair.idOverride}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
deleteRow={deleteCertainRow}
tags={projectTags}
/>
))}
{snapshotData &&
@@ -795,11 +921,14 @@ export default function Dashboard() {
)
.map((keyPair) => (
<KeyPair
isCapitalized={autoCapitalization}
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
@@ -807,11 +936,23 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
tags={projectTags}
/>
))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
<div className='w-10'/>
<button
type="button"
className='text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer'
onClick={addRowToBottom}
>
<FontAwesomeIcon icon={faPlus} className='mr-3'/>
<span className='text-sm'>Add Secret</span>
</button>
</div>
</div>
{!snapshotData && (
<div className="w-full max-w-5xl px-2 pt-3">
<div className="w-full px-2 pt-3">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
@@ -826,7 +967,7 @@ export default function Dashboard() {
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 mt-28">
{isKeyAvailable && !snapshotData && (
<DropZone
setData={setData}
@@ -849,11 +990,38 @@ export default function Dashboard() {
)}
</div>
</div>
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.id === sidebarSecretId
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
</div>
</div>
) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
);
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Github integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Github.requireAuth = true;
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Heroku() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Heroku integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Heroku.requireAuth = true;
+7 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
@@ -141,7 +142,11 @@ export default function Home() {
}, []);
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-20 h-screen">
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-4">
<Head>
<title>Infisical Guide</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-col items-center text-gray-300 text-lg mx-auto max-w-2xl lg:max-w-3xl xl:max-w-4xl py-6">
<div className="text-3xl font-bold text-left w-full">Your quick start guide</div>
<div className="text-md text-left w-full pt-2 pb-4 text-bunker-300">
@@ -167,7 +172,7 @@ export default function Home() {
})}
{learningItem({
text: 'Inject secrets locally',
subText: 'Replace .env files with a more secure an efficient alternative.',
subText: 'Replace .env files with a more secure and efficient alternative.',
complete: false,
icon: faNetworkWired,
time: '8 min',
+90 -98
View File
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter();
@@ -166,80 +163,95 @@ export default function Integrations() {
}
};
/**
* Handle integration option authorization for a given integration option [integrationOption]
* @param {Object} obj
* @param {Object} obj.integrationOption - an integration option
* @param {String} obj.name
* @param {String} obj.type
* @param {String} obj.docsLink
* @returns
*/
const handleIntegrationOption = async ({
integrationOption,
accessToken
}: {
integrationOption: IntegrationOption,
accessToken?: string;
}) => {
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
if (!bot.isActive) {
await handleBotActivate();
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'aws-parameter-store':
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case 'aws-secret-manager':
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
default:
break;
}
if (integrationOption.type === 'oauth') {
// integration is of type OAuth
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
switch (integrationOption.slug) {
case 'heroku':
window.location.assign(
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
);
break;
case 'vercel':
window.location.assign(
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
);
break;
case 'netlify':
window.location.assign(
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
);
break;
case 'github':
window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
);
break;
default:
break;
}
return;
} if (integrationOption.type === 'pat') {
// integration is of type personal access token
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: integrationOption.slug,
accessToken: accessToken ?? ''
});
setIntegrationAuths([...integrationAuths, integrationAuth])
const integration = await createIntegration({
integrationAuthId: integrationAuth._id
});
setIntegrations([...integrations, integration]);
return;
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
}
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'aws-parameter-store':
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'aws-secret-manager':
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/**
* Open dialog to activate bot if bot is not active.
@@ -251,34 +263,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
if (!integrationAuthX) {
// case: integration has not been authorized before
if (integrationOption.type === 'pat') {
// case: integration requires user to input their personal access token for that integration
setIntegrationAccessTokenDialogOpen(true);
return;
}
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
handleIntegrationOption({ integrationOption });
return;
}
if (!bot.isActive) {
await handleBotActivate();
}
// case: integration has been authorized before
// -> create new integration
const integration = await createIntegration({
integrationAuthId: integrationAuthX._id
});
setIntegrations([...integrations, integration]);
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +358,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationAccessTokenDialog
isOpen={isIntegrationAccessTokenDialogOpen}
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
@@ -0,0 +1,100 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function AWSParameterStoreAuthorizeIntegrationPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [accessKey, setAccessKey] = useState('');
const [accessKeyErrorText, setAccessKeyErrorText] = useState('');
const [accessSecretKey, setAccessSecretKey] = useState('');
const [accessSecretKeyErrorText, setAccessSecretKeyErrorText] = useState('');
const handleButtonClick = async () => {
try {
setAccessKeyErrorText('');
setAccessSecretKeyErrorText('');
if (accessKey.length === 0) {
setAccessKeyErrorText('Access key cannot be blank');
return;
}
if (accessSecretKey.length === 0) {
setAccessSecretKeyErrorText('Secret access key cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-parameter-store',
accessId: accessKey,
accessToken: accessSecretKey
});
setAccessKey('');
setAccessSecretKey('');
setIsLoading(false);
router.push(
`/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center mb-4'>AWS Parameter Store Integration</CardTitle>
<FormControl
label="Access Key ID"
errorText={accessKeyErrorText}
isError={accessKeyErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessKey}
onChange={(e) => setAccessKey(e.target.value)}
/>
</FormControl>
<FormControl
label="Secret Access Key"
errorText={accessSecretKeyErrorText}
isError={accessSecretKeyErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessSecretKey}
onChange={(e) => setAccessSecretKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to AWS Parameter Store
</Button>
</Card>
</div>
)
}
AWSParameterStoreAuthorizeIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,178 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const awsRegions = [
{ name: 'US East (Ohio)', slug: 'us-east-2' },
{ name: 'US East (N. Virginia)', slug: 'us-east-1' },
{ name: 'US West (N. California)', slug: 'us-west-1' },
{ name: 'US West (Oregon)', slug: 'us-west-2' },
{ name: 'Africa (Cape Town)', slug: 'af-south-1' },
{ name: 'Asia Pacific (Hong Kong)', slug: 'ap-east-1' },
{ name: 'Asia Pacific (Hyderabad)', slug: 'ap-south-2' },
{ name: 'Asia Pacific (Jakarta)', slug: 'ap-southeast-3' },
{ name: 'Asia Pacific (Melbourne)', slug: 'ap-southeast-4' },
{ name: 'Asia Pacific (Mumbai)', slug: 'ap-south-1' },
{ name: 'Asia Pacific (Osaka)', slug: 'ap-northeast-3' },
{ name: 'Asia Pacific (Seoul)', slug: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', slug: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', slug: 'ap-southeast-2' },
{ name: 'Asia Pacific (Tokyo)', slug: 'ap-northeast-1' },
{ name: 'Canada (Central)', slug: 'ca-central-1' },
{ name: 'Europe (Frankfurt)', slug: 'eu-central-1' },
{ name: 'Europe (Ireland)', slug: 'eu-west-1' },
{ name: 'Europe (London)', slug: 'eu-west-2' },
{ name: 'Europe (Milan)', slug: 'eu-south-1' },
{ name: 'Europe (Paris)', slug: 'eu-west-3' },
{ name: 'Europe (Spain)', slug: 'eu-south-2' },
{ name: 'Europe (Stockholm)', slug: 'eu-north-1' },
{ name: 'Europe (Zurich)', slug: 'eu-central-2' },
{ name: 'Middle East (Bahrain)', slug: 'me-south-1' },
{ name: 'Middle East (UAE)', slug: 'me-central-1' },
{ name: 'South America (Sao Paulo)', slug: 'sa-east-1' },
{ name: 'AWS GovCloud (US-East)', slug: 'us-gov-east-1' },
{ name: 'AWS GovCloud (US-West)', slug: 'us-gov-west-1' },
]
export default function AWSParameterStoreCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [path, setPath] = useState('');
const [pathErrorText, setPathErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setSelectedAWSRegion(awsRegions[0].slug);
}
}, [workspace]);
const isValidAWSParameterStorePath = (secretPath: string) => {
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
return pattern.test(secretPath) && secretPath.length <= 2048;
}
const handleButtonClick = async () => {
try {
if (path !== '') {
// case: path is not empty
if (!isValidAWSParameterStorePath(path)) {
// case: path is not valid for aws parameter store
setPathErrorText('Path must be a valid path for SSM like /project/env/');
return;
}
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: null,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path,
region: selectedAWSRegion
});
setIsLoading(false);
setPathErrorText('');
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>AWS Parameter Store Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="AWS Region"
>
<Select
value={selectedAWSRegion}
onValueChange={(val) => setSelectedAWSRegion(val)}
className='w-full border border-mineshaft-500'
>
{awsRegions.map((awsRegion) => (
<SelectItem value={awsRegion.slug} key={`flyio-environment-${awsRegion.slug}`}>
{awsRegion.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Path"
errorText={pathErrorText}
isError={pathErrorText !== '' ?? false}
>
<Input
placeholder={`/${workspace.name.toLowerCase().replace(/ /g, "-")}/${selectedSourceEnvironment}/`}
value={path}
onChange={(e) => setPath(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AWSParameterStoreCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,100 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [accessKey, setAccessKey] = useState('');
const [accessKeyErrorText, setAccessKeyErrorText] = useState('');
const [accessSecretKey, setAccessSecretKey] = useState('');
const [accessSecretKeyErrorText, setAccessSecretKeyErrorText] = useState('');
const handleButtonClick = async () => {
try {
setAccessKeyErrorText('');
setAccessSecretKeyErrorText('');
if (accessKey.length === 0) {
setAccessKeyErrorText('Access key cannot be blank');
return;
}
if (accessSecretKey.length === 0) {
setAccessSecretKeyErrorText('Secret access key cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-secret-manager',
accessId: accessKey,
accessToken: accessSecretKey
});
setAccessKey('');
setAccessSecretKey('');
setIsLoading(false);
router.push(
`/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center mb-4'>AWS Secret Manager Integration</CardTitle>
<FormControl
label="Access Key ID"
errorText={accessKeyErrorText}
isError={accessKeyErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessKey}
onChange={(e) => setAccessKey(e.target.value)}
/>
</FormControl>
<FormControl
label="Secret Access Key"
errorText={accessSecretKeyErrorText}
isError={accessSecretKeyErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessSecretKey}
onChange={(e) => setAccessSecretKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to AWS Secret Manager
</Button>
</Card>
</div>
);
}
AWSSecretManagerCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,177 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const awsRegions = [
{ name: 'US East (Ohio)', slug: 'us-east-2' },
{ name: 'US East (N. Virginia)', slug: 'us-east-1' },
{ name: 'US West (N. California)', slug: 'us-west-1' },
{ name: 'US West (Oregon)', slug: 'us-west-2' },
{ name: 'Africa (Cape Town)', slug: 'af-south-1' },
{ name: 'Asia Pacific (Hong Kong)', slug: 'ap-east-1' },
{ name: 'Asia Pacific (Hyderabad)', slug: 'ap-south-2' },
{ name: 'Asia Pacific (Jakarta)', slug: 'ap-southeast-3' },
{ name: 'Asia Pacific (Melbourne)', slug: 'ap-southeast-4' },
{ name: 'Asia Pacific (Mumbai)', slug: 'ap-south-1' },
{ name: 'Asia Pacific (Osaka)', slug: 'ap-northeast-3' },
{ name: 'Asia Pacific (Seoul)', slug: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', slug: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', slug: 'ap-southeast-2' },
{ name: 'Asia Pacific (Tokyo)', slug: 'ap-northeast-1' },
{ name: 'Canada (Central)', slug: 'ca-central-1' },
{ name: 'Europe (Frankfurt)', slug: 'eu-central-1' },
{ name: 'Europe (Ireland)', slug: 'eu-west-1' },
{ name: 'Europe (London)', slug: 'eu-west-2' },
{ name: 'Europe (Milan)', slug: 'eu-south-1' },
{ name: 'Europe (Paris)', slug: 'eu-west-3' },
{ name: 'Europe (Spain)', slug: 'eu-south-2' },
{ name: 'Europe (Stockholm)', slug: 'eu-north-1' },
{ name: 'Europe (Zurich)', slug: 'eu-central-2' },
{ name: 'Middle East (Bahrain)', slug: 'me-south-1' },
{ name: 'Middle East (UAE)', slug: 'me-central-1' },
{ name: 'South America (Sao Paulo)', slug: 'sa-east-1' },
{ name: 'AWS GovCloud (US-East)', slug: 'us-gov-east-1' },
{ name: 'AWS GovCloud (US-West)', slug: 'us-gov-west-1' },
]
export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [targetSecretName, setTargetSecretName] = useState('');
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
// const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setSelectedAWSRegion(awsRegions[0].slug);
}
}, [workspace]);
// const isValidAWSPath = (path: string) => {
// const pattern = /^\/[\w./]+\/$/;
// return pattern.test(path) && path.length <= 2048;
// }
const handleButtonClick = async () => {
try {
if (targetSecretName.trim() === '') {
setTargetSecretName('Secret name cannot be blank');
return;
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetSecretName.trim(),
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: selectedAWSRegion
});
setIsLoading(false);
setTargetSecretNameErrorText('');
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>AWS Secret Manager Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="AWS Region"
>
<Select
value={selectedAWSRegion}
onValueChange={(val) => setSelectedAWSRegion(val)}
className='w-full border border-mineshaft-500'
>
{awsRegions.map((awsRegion) => (
<SelectItem value={awsRegion.slug} key={`flyio-environment-${awsRegion.slug}`}>
{awsRegion.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="AWS SM Secret Name"
errorText={targetSecretNameErrorText}
isError={targetSecretNameErrorText !== '' ?? false}
>
<Input
placeholder={`${workspace.name.toLowerCase().replace(/ /g, "-")}/${selectedSourceEnvironment}`}
value={targetSecretName}
onChange={(e) => setTargetSecretName(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AWSSecretManagerCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function AzureKeyVaultCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
const handleButtonClick = async () => {
try {
if (vaultBaseUrl.length === 0) {
setVaultBaseUrlErrorText('Vault URI cannot be blank');
return;
}
if (
!vaultBaseUrl.startsWith('https://')
|| !vaultBaseUrl.endsWith('vault.azure.net')
) {
setVaultBaseUrlErrorText('Vault URI must be like https://<vault_name>.vault.azure.net');
return;
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultBaseUrl,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Azure Key Vault Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vault URI"
errorText={vaultBaseUrlErrorText}
isError={vaultBaseUrlErrorText !== '' ?? false}
>
<Input
placeholder='https://example.vault.azure.net'
value={vaultBaseUrl}
onChange={(e) => setVaultBaseUrl(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AzureKeyVaultCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function AzureKeyVaultOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'azure-key-vault'
});
router.push(
`/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,77 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function FlyioCreateIntegrationPage() {
const router = useRouter();
const [accessToken, setAccessToken] = useState('');
const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setAccessTokenErrorText('');
if (accessToken.length === 0) {
setAccessTokenErrorText('Access token cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessId: null,
accessToken
});
setIsLoading(false);
router.push(
`/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Fly.io Integration</CardTitle>
<FormControl
label="Fly.io Access Token"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Fly.io
</Button>
</Card>
</div>
)
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function FlyioCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Fly.io Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Fly.io App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,125 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function GitHubCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [owner, setOwner] = useState<string | null>(null);
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
setOwner(integrationAuthApps[0]?.owner ?? null);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>GitHub Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="GitHub Repo"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`github-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
GitHubCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function GitHubOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github'
});
router.push(
`/integrations/github/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
GitHubOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function HerokuCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Heroku Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Heroku App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
HerokuCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function HerokuOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku'
});
router.push(
`/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
HerokuOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,145 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const netlifyEnvironments = [
{ name: 'Local development', slug: 'dev' },
{ name: 'Branch deploys', slug: 'branch-deploy' },
{ name: 'Deploy previews', slug: 'deploy-preview' },
{ name: 'Production', slug: 'production' }
]
export default function NetlifyCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setTargetEnvironment(netlifyEnvironments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Netlify Integration</CardTitle>
<FormControl
label="Project Environment"
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Site"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Context"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{netlifyEnvironments.map((netlifyEnvironment) => (
<SelectItem value={netlifyEnvironment.slug} key={`netlify-environment-${netlifyEnvironment.slug}`}>
{netlifyEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
NetlifyCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function NetlifyOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify'
});
router.push(
`/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
NetlifyOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,77 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function RenderCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState('');
const [apiKeyErrorText, setApiKeyErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText('');
if (apiKey.length === 0) {
setApiKeyErrorText('API Key cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'render',
accessId: null,
accessToken: apiKey
});
setIsLoading(false);
router.push(
`/integrations/render/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Render API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder='rnd_xxx'
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Render
</Button>
</Card>
</div>
)
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function RenderCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Render Service"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,144 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const vercelEnvironments = [
{ name: 'Development', slug: 'development' },
{ name: 'Preview', slug: 'preview' },
{ name: 'Production', slug: 'production' }
]
export default function VercelCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironemnt] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
setTargetEnvironemnt(vercelEnvironments[0].slug);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Vercel Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App Environment"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironemnt(val)}
className='w-full border border-mineshaft-500'
>
{vercelEnvironments.map((vercelEnvironment) => (
<SelectItem value={vercelEnvironment.slug} key={`vercel-environment-${vercelEnvironment.slug}`}>
{vercelEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
VercelCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function VercelOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel'
});
router.push(
`/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
VercelOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
+11 -1
View File
@@ -13,6 +13,7 @@ import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox';
import attemptLogin from '@app/components/utilities/attemptLogin';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
@@ -21,6 +22,7 @@ export default function Login() {
const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const router = useRouter();
const { t } = useTranslation();
const lang = router.locale ?? 'en';
@@ -31,6 +33,7 @@ export default function Login() {
};
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
let userWorkspace;
try {
@@ -41,7 +44,10 @@ export default function Login() {
console.log('Error - Not logged in yet');
}
};
redirectToDashboard();
if (isLoggedIn()) {
setIsAlreadyLoggedIn(true);
redirectToDashboard();
}
}, []);
/**
@@ -60,6 +66,10 @@ export default function Login() {
});
};
if (isAlreadyLoggedIn) {
return null
}
return (
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
<Head>
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Netlify() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
// modify comment here
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Netlify integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Netlify.requireAuth = true;
+3 -2
View File
@@ -66,8 +66,9 @@ export default function SettingsBilling() {
const subscriptions = await getOrganizationSubscriptions({
orgId
});
setCurrentPlan(subscriptions.data[0].plan.product);
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product);
}
const orgUsers = await getOrganizationUsers({
orgId
});
+6 -4
View File
@@ -87,7 +87,9 @@ export default function SettingsOrg() {
const subscriptions = await getOrganizationSubscriptions({
orgId
});
setCurrentPlan(subscriptions.data[0].plan.product);
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, []);
@@ -173,7 +175,7 @@ export default function SettingsOrg() {
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-8xl">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
<div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start w-full mb-6">
@@ -229,7 +231,7 @@ export default function SettingsOrg() {
text="You can add more members if you switch to Infisical's Team plan."
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="pb-1 w-full flex flex-row items-start max-w-6xl">
<div className="pb-1 w-full flex flex-row items-start">
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
@@ -253,7 +255,7 @@ export default function SettingsOrg() {
</div>
</div>
{userList && (
<div className="overflow-y-auto max-w-6xl w-full">
<div className="overflow-y-auto w-full">
<UserTable
userData={userList}
changeData={setUserList}
+13 -316
View File
@@ -1,332 +1,29 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import AddServiceTokenDialog from '@app/components/basic/dialog/AddServiceTokenDialog';
import InputField from '@app/components/basic/InputField';
import EnvironmentTable from '@app/components/basic/table/EnvironmentsTable';
import ServiceTokenTable from '@app/components/basic/table/ServiceTokenTable';
import NavHeader from '@app/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import deleteEnvironment from '@app/pages/api/environments/deleteEnvironment';
import updateEnvironment from '@app/pages/api/environments/updateEnvironment';
import createEnvironment from '../../api/environments/createEnvironment';
import getServiceTokens from '../../api/serviceToken/getServiceTokens';
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
import getWorkspaces from '../../api/workspace/getWorkspaces';
import renameWorkspace from '../../api/workspace/renameWorkspace';
type EnvData = {
name: string;
slug: string;
};
export default function SettingsBasic() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceName, setWorkspaceName] = useState('');
const [serviceTokens, setServiceTokens] = useState<any[]>([]);
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
// const [isAddOpen, setIsAddOpen] = useState(false);
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
const [projectIdCopied, setProjectIdCopied] = useState(false);
const workspaceId = router.query.id as string;
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { ProjectSettingsPage } from '@app/views/Settings/ProjectSettingsPage';
const ProjectSettings = () => {
const { t } = useTranslation();
/**
* This function copies the project id to the clipboard
*/
function copyToClipboard() {
const copyText = document.getElementById('myInput') as HTMLInputElement;
if (copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value);
setProjectIdCopied(true);
setTimeout(() => setProjectIdCopied(false), 2000);
}
}
useEffect(() => {
const load = async () => {
const userWorkspaces = await getWorkspaces();
userWorkspaces.forEach((userWorkspace) => {
if (userWorkspace._id === workspaceId) {
setWorkspaceName(userWorkspace.name);
setEnvironments(userWorkspace.environments);
}
});
const tempServiceTokens = await getServiceTokens({
workspaceId
});
setServiceTokens(tempServiceTokens);
};
load();
}, []);
const modifyWorkspaceName = (newName: string) => {
setButtonReady(true);
setWorkspaceName(newName);
};
const submitChanges = (newWorkspaceName: string) => {
renameWorkspace(workspaceId, newWorkspaceName);
setButtonReady(false);
};
const closeAddServiceTokenModal = () => {
setIsAddServiceTokenDialogOpen(false);
};
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
*/
const executeDeletingWorkspace = async () => {
const userWorkspaces = await getWorkspaces();
if (userWorkspaces.length > 1) {
if (
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
workspaceToBeDeletedName
) {
await deleteWorkspace(workspaceId);
const ws = await getWorkspaces();
router.push(`/dashboard/${ws[0]._id}`);
}
}
};
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
const res = await createEnvironment(workspaceId, {
environmentName: name,
environmentSlug: slug
});
if (res) {
// TODO: on react-query migration do an api call to resync
setEnvironments((env) => [...env, { name, slug }]);
}
};
const onUpdateEnvironment = async (oldSlug: string, { name, slug }: EnvData) => {
const res = await updateEnvironment(workspaceId, {
oldEnvironmentSlug: oldSlug,
environmentName: name,
environmentSlug: slug
});
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) => env.map((el) => (el.slug === oldSlug ? { name, slug } : el)));
}
};
const onDeleteEnvironment = async (slugToBeDelete: string) => {
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) => env.filter(({ slug }) => slug !== slugToBeDelete));
}
};
return (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<>
<Head>
<title>{t('common:head-title', { title: t('settings-project:title') })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen}
workspaceId={workspaceId}
environments={environments}
closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/>
<div className="flex flex-row mr-6 max-w-5xl">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl">
<p className="font-semibold mr-4 text-gray-200">{t('settings-project:title')}</p>
<p className="font-normal mr-4 text-gray-400 text-base">
{t('settings-project:description')}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-start">
<div className="bg-white/5 rounded-md px-6 pb-4 flex flex-col items-start w-full mb-6 pt-2">
<p className="text-xl font-semibold mb-4 mt-2">{t('common:display-name')}</p>
<div className="max-h-28 w-full max-w-md mr-auto">
<InputField
label=""
onChangeHandler={modifyWorkspaceName}
type="varName"
value={workspaceName}
placeholder=""
isRequired
/>
</div>
<div className="flex justify-start w-full">
<div className="flex justify-start max-w-sm mt-4 mb-2">
<Button
text={t('common:save-changes') as string}
onButtonPressed={() => submitChanges(workspaceName)}
color="mineshaft"
size="md"
active={buttonReady}
iconDisabled={faCheck}
textDisabled="Saved"
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start">{t('common:project-id')}</p>
<p className="text-base text-gray-400 font-normal self-start mt-4">
{t('settings-project:project-id-description')}
</p>
<p className="text-base text-gray-400 font-normal self-start">
{t('settings-project:project-id-description2')}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary hover:opacity-80 duration-200"
>
{t('settings-project:docs')}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">
{t('settings-project:auto-generated')}
</p>
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
<p className="mr-2 font-bold pl-4">{`${t('common:project-id')}:`}</p>
<input
type="text"
value={workspaceId}
id="myInput"
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
disabled
/>
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button
type="button"
onClick={copyToClipboard}
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
{t('common:click-to-copy')}
</span>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4">
<EnvironmentTable
data={environments}
onCreateEnv={onCreateEnvironment}
onUpdateEnv={onUpdateEnvironment}
onDeleteEnv={onDeleteEnvironment}
/>
</div>
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3">
{t('section-token:service-tokens')}
</p>
<p className="text-sm text-gray-400">
{t('section-token:service-tokens-description')}
</p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>
.
</p>
</div>
<div className="w-48 mt-2">
<Button
text={t('section-token:add-new') as string}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens as any}
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="mt-2 text-md text-gray-400">{t('settings-project:danger-zone-note')}</p>
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
<InputField
label={t('settings-project:project-to-delete') as string}
onChangeHandler={setWorkspaceToBeDeletedName}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
{t('settings-project:delete-project')}
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
{t('settings-project:delete-project-note')}
</p>
</div>
</div>
</div>
</div>
</div>
<ProjectSettingsPage />
</>
);
}
};
SettingsBasic.requireAuth = true;
export default ProjectSettings;
export const getServerSideProps = getTranslatedServerSideProps([
ProjectSettings.requireAuth = true;
export const getServerSideProps = getTranslatedStaticProps([
'settings',
'settings-project',
'section-token'
+7 -8
View File
@@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import AddProjectMemberDialog from '@app/components/basic/dialog/AddProjectMemberDialog';
import UserTable from '@app/components/basic/table/UserTable';
import ProjectUsersTable from '@app/components/basic/table/ProjectUsersTable';
import NavHeader from '@app/components/navigation/NavHeader';
import guidGenerator from '@app/components/utilities/randomId';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
@@ -33,6 +33,7 @@ interface UserProps {
}
interface MembershipProps {
deniedPermissions: any[];
user: UserProps;
inviteEmail: string;
role: string;
@@ -76,6 +77,7 @@ export default function Users() {
status: membership?.status,
userId: membership.user?._id,
membershipId: membership._id,
deniedPermissions: membership.deniedPermissions,
publicKey: membership.user?.publicKey
}));
setUserList(tempUserList);
@@ -144,7 +146,7 @@ export default function Users() {
};
return userList ? (
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start">
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start max-w-[calc(100vw-240px)]">
<Head>
<title>{t('common:head-title', { title: t('settings-members:title') })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -168,7 +170,7 @@ export default function Users() {
setEmail={setEmail}
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="px-6 pb-1 w-full flex flex-row items-start min-w-6xl max-w-6xl">
<div className="px-6 pb-1 w-full flex flex-row items-start">
<div className="h-10 w-full bg-white/5 mt-2 rounded-md flex flex-row items-center">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
@@ -191,14 +193,12 @@ export default function Users() {
/>
</div>
</div>
<div className="block overflow-y-auto min-w-6xl max-w-6xl px-6">
<UserTable
<div className="block overflow-x-scroll px-6 pb-6 no-scrollbar no-scrollbar::-webkit-scrollbar">
<ProjectUsersTable
userData={userList}
changeData={setUserList}
myUser={personalEmail}
filter={searchUsers}
resendInvite={submitAddModal}
isOrg={false}
// onClick={openDeleteModal}
// deleteUser={deleteMembership}
// setUserIdToBeDeleted={setUserIdToBeDeleted}
@@ -207,7 +207,6 @@ export default function Users() {
</div>
) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
);
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Vercel() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
// type check
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Vercel integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Vercel.requireAuth = true;
+1 -1
View File
@@ -29,7 +29,7 @@ export default function VerifyEmail() {
<title>Login</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Log In to Infisical" />
<meta property="og:title" content="Verify your email in Infisical" />
<meta
name="og:description"
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."