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 };