diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars
index f76c42ba5..045a9602b 100644
--- a/backend/src/templates/organizationInvitation.handlebars
+++ b/backend/src/templates/organizationInvitation.handlebars
@@ -8,7 +8,7 @@
Join your organization on Infisical
- {{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}
+ {{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}
Join now
What is Infisical?
Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.
diff --git a/backend/src/templates/workspaceInvitation.handlebars b/backend/src/templates/workspaceInvitation.handlebars
index e899d86e6..60556555c 100644
--- a/backend/src/templates/workspaceInvitation.handlebars
+++ b/backend/src/templates/workspaceInvitation.handlebars
@@ -7,7 +7,7 @@
Join your team on Infisical
- {{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}
+ {{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}
Join now
What is Infisical?
Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.
diff --git a/frontend/public/locales/en/nav.json b/frontend/public/locales/en/nav.json
index de5218619..3d0c6faca 100644
--- a/frontend/public/locales/en/nav.json
+++ b/frontend/public/locales/en/nav.json
@@ -1,6 +1,6 @@
{
"support": {
- "slack": "[NEW] Join Slack Forum",
+ "slack": "Join Slack Forum",
"docs": "Read Docs",
"issue": "Open a Github Issue",
"email": "Send us an Email"
diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx
index bc3876750..57a3f96fd 100644
--- a/frontend/src/components/navigation/NavHeader.tsx
+++ b/frontend/src/components/navigation/NavHeader.tsx
@@ -1,10 +1,7 @@
-import { useEffect, useState } from 'react';
-import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { useWorkspace } from '@app/context';
-import getOrganization from '@app/pages/api/organization/GetOrg';
+import { useOrganization, useWorkspace } from '@app/context';
/**
* This is the component at the top of almost every page.
@@ -22,28 +19,15 @@ export default function NavHeader({
pageName: string;
isProjectRelated?: boolean;
}): JSX.Element {
- const [orgName, setOrgName] = useState('');
- const router = useRouter();
- const projectId = String(router.query.id);
const { currentWorkspace } = useWorkspace();
-
- useEffect(() => {
- (async () => {
- const orgId = localStorage.getItem('orgData.id');
- const org = await getOrganization({
- orgId: orgId || ''
- });
- setOrgName(org.name);
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [projectId]);
+ const { currentOrg } = useOrganization();
return (
- {orgName?.charAt(0)}
+ {currentOrg?.name?.charAt(0)}
-
{orgName}
+
{currentOrg?.name}
{isProjectRelated && (
<>
diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx
index df12c25f7..af59f3d51 100644
--- a/frontend/src/components/v2/Button/Button.tsx
+++ b/frontend/src/components/v2/Button/Button.tsx
@@ -24,7 +24,7 @@ const buttonVariants = cva(
{
variants: {
colorSchema: {
- primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
+ primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},
diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx
index 1f8448a03..4bb3c3674 100644
--- a/frontend/src/components/v2/Card/Card.tsx
+++ b/frontend/src/components/v2/Card/Card.tsx
@@ -8,9 +8,9 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
-
+
{children}
- {subTitle &&
{subTitle}
}
+ {subTitle &&
{subTitle}
}
);
diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx
index 5b30fb458..c2ecfad14 100644
--- a/frontend/src/components/v2/Checkbox/Checkbox.tsx
+++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx
@@ -28,8 +28,9 @@ export const Checkbox = ({
-
+
diff --git a/frontend/src/components/v2/Menu/Menu.tsx b/frontend/src/components/v2/Menu/Menu.tsx
index 1497b01fa..262c618f9 100644
--- a/frontend/src/components/v2/Menu/Menu.tsx
+++ b/frontend/src/components/v2/Menu/Menu.tsx
@@ -31,18 +31,20 @@ export const MenuItem =
({
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
- inputRef
+ inputRef,
+ ...props
}: MenuItemProps & ComponentPropsWithRef): JSX.Element => (
- -
- {icon && {icon}}
+
-
+
+ {icon && {icon}}
{children}
{description && {description}}
diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx
index efc6ba3a3..c33881772 100644
--- a/frontend/src/components/v2/Modal/Modal.tsx
+++ b/frontend/src/components/v2/Modal/Modal.tsx
@@ -25,7 +25,7 @@ export const ModalContent = forwardRef(
@@ -36,7 +36,7 @@ export const ModalContent = forwardRef(
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx
index 86ccc1079..5a1aa7a35 100644
--- a/frontend/src/components/v2/Select/Select.tsx
+++ b/frontend/src/components/v2/Select/Select.tsx
@@ -12,13 +12,14 @@ type Props = {
className?: string;
dropdownContainerClassName?: string;
isLoading?: boolean;
+ position?: 'item-aligned' | 'popper';
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef(
(
- { children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
+ { children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
ref
): JSX.Element => {
return (
@@ -44,11 +45,13 @@ export const Select = forwardRef(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName
)}
+ position={position}
+ style={{ width: 'var(--radix-select-trigger-width)' }}
>
-
+
{isLoading ? (
@@ -82,7 +85,7 @@ export const SelectItem = forwardRef
(
{...props}
className={twMerge(
`relative flex cursor-pointer
- select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
+ select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
@@ -90,8 +93,8 @@ export const SelectItem = forwardRef(
)}
ref={forwardedRef}
>
-
-
+
+
{children}
diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx
new file mode 100644
index 000000000..31a97d07c
--- /dev/null
+++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx
@@ -0,0 +1,46 @@
+import { createContext, ReactNode, useContext, useMemo } from 'react';
+
+import { useGetOrganization } from '@app/hooks/api';
+import { Organization } from '@app/hooks/api/types';
+
+import { useWorkspace } from '../WorkspaceContext';
+
+type TOrgContext = {
+ orgs?: Organization[];
+ currentOrg?: Organization;
+ isLoading: boolean;
+};
+
+const OrgContext = createContext(null);
+
+type Props = {
+ children: ReactNode;
+};
+
+export const OrgProvider = ({ children }: Props): JSX.Element => {
+ const { currentWorkspace } = useWorkspace();
+ const { data: userOrgs, isLoading } = useGetOrganization();
+
+ const currentWsOrgID = currentWorkspace?.organization;
+
+ // memorize the workspace details for the context
+ const value = useMemo(
+ () => ({
+ orgs: userOrgs,
+ currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
+ isLoading
+ }),
+ [currentWsOrgID, userOrgs, isLoading]
+ );
+
+ return {children};
+};
+
+export const useOrganization = () => {
+ const ctx = useContext(OrgContext);
+ if (!ctx) {
+ throw new Error('useOrganization to be used within ');
+ }
+
+ return ctx;
+};
diff --git a/frontend/src/context/OrganizationContext/index.tsx b/frontend/src/context/OrganizationContext/index.tsx
new file mode 100644
index 000000000..ac9091e35
--- /dev/null
+++ b/frontend/src/context/OrganizationContext/index.tsx
@@ -0,0 +1 @@
+export { OrgProvider, useOrganization } from './OrganizationContext';
diff --git a/frontend/src/context/UserContext/UserContext.tsx b/frontend/src/context/UserContext/UserContext.tsx
new file mode 100644
index 000000000..0c6ce9549
--- /dev/null
+++ b/frontend/src/context/UserContext/UserContext.tsx
@@ -0,0 +1,38 @@
+import { createContext, ReactNode, useContext, useMemo } from 'react';
+
+import { useGetUser } from '@app/hooks/api';
+import { User } from '@app/hooks/api/types';
+
+type TUserContext = {
+ user: User;
+ isLoading: boolean;
+};
+
+const UserContext = createContext(null);
+
+type Props = {
+ children: ReactNode;
+};
+
+export const UserProvider = ({ children }: Props): JSX.Element => {
+ const { data, isLoading } = useGetUser();
+
+ // memorize the workspace details for the context
+ const value = useMemo(() => {
+ return {
+ user: data!,
+ isLoading
+ };
+ }, [data, isLoading]);
+
+ return {children};
+};
+
+export const useUser = () => {
+ const ctx = useContext(UserContext);
+ if (!ctx) {
+ throw new Error('useUser has to be used within ');
+ }
+
+ return ctx;
+};
diff --git a/frontend/src/context/UserContext/index.tsx b/frontend/src/context/UserContext/index.tsx
new file mode 100644
index 000000000..44aa26a3b
--- /dev/null
+++ b/frontend/src/context/UserContext/index.tsx
@@ -0,0 +1 @@
+export { UserProvider, useUser } from './UserContext';
diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx
index 4edef8e6a..f2876d996 100644
--- a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx
+++ b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx
@@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
// memorize the workspace details for the context
const value = useMemo(() => {
+ const wsId = workspaceId || localStorage.getItem('projectData.id');
return {
workspaces: ws || [],
- currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
+ currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
isLoading
};
}, [ws, workspaceId, isLoading]);
diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx
index 1bf59f9ac..215802f5d 100644
--- a/frontend/src/context/index.tsx
+++ b/frontend/src/context/index.tsx
@@ -1,2 +1,5 @@
+export { AuthProvider } from './AuthContext';
+export { OrgProvider, useOrganization } from './OrganizationContext';
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
+export { UserProvider, useUser } from './UserContext';
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index 263f7e152..ad9acf0c3 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -1,6 +1,8 @@
export * from './auth';
export * from './keys';
+export * from './organization';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
+export * from './users';
export * from './workspace';
diff --git a/frontend/src/hooks/api/keys/index.tsx b/frontend/src/hooks/api/keys/index.tsx
index 23ffdf8d1..7b4286b3a 100644
--- a/frontend/src/hooks/api/keys/index.tsx
+++ b/frontend/src/hooks/api/keys/index.tsx
@@ -1 +1 @@
-export { useGetUserWsKey } from './queries';
+export { useGetUserWsKey, useUploadWsKey } from './queries';
diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx
index d37c73f13..6d7819dce 100644
--- a/frontend/src/hooks/api/keys/queries.tsx
+++ b/frontend/src/hooks/api/keys/queries.tsx
@@ -1,8 +1,8 @@
-import { useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
-import { UserWsKeyPair } from './types';
+import { UploadWsKeyDTO, UserWsKeyPair } from './types';
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
@@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
+
+// mutations
+export const useUploadWsKey = () =>
+ useMutation<{}, {}, UploadWsKeyDTO>({
+ mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
+ apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
+ });
diff --git a/frontend/src/hooks/api/keys/types.ts b/frontend/src/hooks/api/keys/types.ts
index bad61b5dd..ee4660aeb 100644
--- a/frontend/src/hooks/api/keys/types.ts
+++ b/frontend/src/hooks/api/keys/types.ts
@@ -20,3 +20,10 @@ export type Sender = {
lastName: string;
publicKey: string;
};
+
+export type UploadWsKeyDTO = {
+ userId: string;
+ encryptedKey: string;
+ nonce: string;
+ workspaceId: string;
+};
diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts
new file mode 100644
index 000000000..4d9dd04f8
--- /dev/null
+++ b/frontend/src/hooks/api/organization/index.ts
@@ -0,0 +1 @@
+export { useGetOrganization } from './queries';
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
new file mode 100644
index 000000000..b2f9c7ce6
--- /dev/null
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -0,0 +1,18 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import { Organization } from './types';
+
+const organizationKeys = {
+ getUserOrganization: ['organization'] as const
+};
+
+const fetchUserOrganization = async () => {
+ const { data } = await apiRequest.get<{ organizations: Organization[] }>('/api/v1/organization');
+
+ return data.organizations;
+};
+
+export const useGetOrganization = () =>
+ useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
new file mode 100644
index 000000000..74720320b
--- /dev/null
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -0,0 +1,6 @@
+export type Organization = {
+ _id: string;
+ name: string;
+ createAt: string;
+ updatedAt: string;
+};
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
index a0475e24e..699e9a81d 100644
--- a/frontend/src/hooks/api/types.ts
+++ b/frontend/src/hooks/api/types.ts
@@ -1,7 +1,9 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { UserWsKeyPair } from './keys/types';
+export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
+export type { User } from './users/types';
export type {
CreateEnvironmentDTO,
DeleteEnvironmentDTO,
diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx
new file mode 100644
index 000000000..69c0fc98a
--- /dev/null
+++ b/frontend/src/hooks/api/users/index.tsx
@@ -0,0 +1,7 @@
+export {
+ fetchOrgUsers,
+ useAddUserToWs,
+ useGetOrgUsers,
+ useGetUser,
+ useLogoutUser
+} from './queries';
diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx
new file mode 100644
index 000000000..1ddcf3809
--- /dev/null
+++ b/frontend/src/hooks/api/users/queries.tsx
@@ -0,0 +1,84 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+
+import {
+ decryptAssymmetric,
+ encryptAssymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { apiRequest } from '@app/config/request';
+import { setAuthToken } from '@app/reactQuery';
+
+import { useUploadWsKey } from '../keys/queries';
+import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
+
+const userKeys = {
+ getUser: ['user'] as const,
+ getOrgUsers: (orgId: string) => [{ orgId }, 'user']
+};
+
+const fetchUserDetails = async () => {
+ const { data } = await apiRequest.get<{ user: User }>('/api/v1/user');
+
+ return data.user;
+};
+
+export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
+
+export const fetchOrgUsers = async (orgId: string) => {
+ const { data } = await apiRequest.get<{ users: OrgUser[] }>(
+ `/api/v1/organization/${orgId}/users`
+ );
+
+ return data.users;
+};
+
+export const useGetOrgUsers = (orgId: string) =>
+ useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
+
+// mutation
+export const useAddUserToWs = () => {
+ const uploadWsKey = useUploadWsKey();
+
+ return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
+ mutationFn: ({ email, workspaceId }) =>
+ apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }),
+ onSuccess: ({ data }, { workspaceId }) => {
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
+ if (!PRIVATE_KEY) return;
+
+ // assymmetrically decrypt symmetric key with local private key
+ const key = decryptAssymmetric({
+ ciphertext: data.latestKey.encryptedKey,
+ nonce: data.latestKey.nonce,
+ publicKey: data.latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
+ plaintext: key,
+ publicKey: data.invitee.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ uploadWsKey.mutate({
+ encryptedKey: inviteeCipherText,
+ nonce: inviteeNonce,
+ userId: data.invitee._id,
+ workspaceId
+ });
+ }
+ });
+};
+
+export const useLogoutUser = () =>
+ useMutation({
+ mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
+ onSuccess: () => {
+ setAuthToken('');
+ // Delete the cookie by not setting a value; Alternatively clear the local storage
+ localStorage.setItem('publicKey', '');
+ localStorage.setItem('encryptedPrivateKey', '');
+ localStorage.setItem('iv', '');
+ localStorage.setItem('tag', '');
+ localStorage.setItem('PRIVATE_KEY', '');
+ }
+ });
diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts
new file mode 100644
index 000000000..6f7552d54
--- /dev/null
+++ b/frontend/src/hooks/api/users/types.ts
@@ -0,0 +1,39 @@
+import { UserWsKeyPair } from '../keys/types';
+
+export type User = {
+ seenIps: string[];
+ _id: string;
+ email: string;
+ createdAt: Date;
+ updatedAt: Date;
+ __v: number;
+ firstName: string;
+ lastName: string;
+ publicKey: string;
+};
+
+export type OrgUser = {
+ _id: string;
+ user: {
+ email: string;
+ firstName: string;
+ lastName: string;
+ _id: string;
+ publicKey: string;
+ };
+ inviteEmail: string;
+ organization: string;
+ role: 'owner' | 'admin' | 'member';
+ status: 'invited' | 'accepted';
+ deniedPermissions: any[];
+};
+
+export type AddUserToWsDTO = {
+ workspaceId: string;
+ email: string;
+};
+
+export type AddUserToWsRes = {
+ invitee: OrgUser['user'];
+ latestKey: UserWsKeyPair;
+};
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 51f8c38eb..52a01029e 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -1,4 +1,5 @@
export {
+ useCreateWorkspace,
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index bae1db874..0c4ba970b 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -4,6 +4,7 @@ import { apiRequest } from '@app/config/request';
import {
CreateEnvironmentDTO,
+ CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
@@ -40,6 +41,18 @@ export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
// mutation
+export const useCreateWorkspace = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
+ mutationFn: async ({ organizationId, workspaceName }) =>
+ apiRequest.post('/api/v1/workspace', { workspaceName, organizationId }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
export const useRenameWorkspace = () => {
const queryClient = useQueryClient();
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 312a811d9..738f5188c 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -11,6 +11,11 @@ export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
+export type CreateWorkspaceDTO = {
+ workspaceName: string;
+ organizationId: string;
+};
+
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
new file mode 100644
index 000000000..3f4e0f95e
--- /dev/null
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -0,0 +1,427 @@
+/* eslint-disable no-nested-ternary */
+/* eslint-disable no-unexpected-multiline */
+/* eslint-disable react-hooks/exhaustive-deps */
+import crypto from 'crypto';
+
+import { useEffect, useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
+import {
+ faBookOpen,
+ faFileLines,
+ faGear,
+ faKey,
+ faMobile,
+ faPlug,
+ faPlus,
+ faUser
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
+import { tempLocalStorage } from '@app/components/utilities/checks/tempLocalStorage';
+import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
+import {
+ Button,
+ Checkbox,
+ FormControl,
+ Input,
+ Menu,
+ MenuItem,
+ Modal,
+ ModalContent,
+ Select,
+ SelectItem
+} from '@app/components/v2';
+import { useOrganization, useUser, useWorkspace } from '@app/context';
+import { usePopUp } from '@app/hooks';
+import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from '@app/hooks/api';
+import getOrganizations from '@app/pages/api/organization/getOrgs';
+import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
+
+import { Navbar } from './components/NavBar';
+
+interface LayoutProps {
+ children: React.ReactNode;
+}
+
+const formSchema = yup.object({
+ name: yup.string().required().label('Project Name').trim(),
+ addMembers: yup.bool().required().label('Add Members')
+});
+
+type TAddProjectFormData = yup.InferType;
+
+export const AppLayout = ({ children }: LayoutProps) => {
+ const router = useRouter();
+ const { createNotification } = useNotificationContext();
+
+ // eslint-disable-next-line prefer-const
+ let { workspaces, currentWorkspace } = useWorkspace();
+ const { currentOrg } = useOrganization();
+ workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id)
+ const { user } = useUser();
+
+ const createWs = useCreateWorkspace();
+ const uploadWsKey = useUploadWsKey();
+ const addWsUser = useAddUserToWs();
+
+ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
+ 'addNewWs'
+ ] as const);
+ const {
+ control,
+ formState: { isSubmitting },
+ reset,
+ handleSubmit
+ } = useForm({
+ resolver: yupResolver(formSchema)
+ });
+
+ const [workspaceMapping, setWorkspaceMapping] = useState