@@ -0,0 +1,66 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { stripeCredentialsSchema } from 'models/features/blocks/inputs/payment/schemas'
|
||||
import { googleSheetsCredentialsSchema } from 'models/features/blocks/integrations/googleSheets/schemas'
|
||||
import { openAICredentialsSchema } from 'models/features/blocks/integrations/openai'
|
||||
import { smtpCredentialsSchema } from 'models/features/blocks/integrations/sendEmail'
|
||||
import { encrypt } from 'utils/api/encryption'
|
||||
import { z } from 'zod'
|
||||
|
||||
const inputShape = {
|
||||
data: true,
|
||||
type: true,
|
||||
workspaceId: true,
|
||||
name: true,
|
||||
} as const
|
||||
|
||||
export const createCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/credentials',
|
||||
protect: true,
|
||||
summary: 'Create credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
credentials: z.discriminatedUnion('type', [
|
||||
stripeCredentialsSchema.pick(inputShape),
|
||||
smtpCredentialsSchema.pick(inputShape),
|
||||
googleSheetsCredentialsSchema.pick(inputShape),
|
||||
openAICredentialsSchema.pick(inputShape),
|
||||
]),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { credentials }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: credentials.workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
const { encryptedData, iv } = encrypt(credentials.data)
|
||||
const createdCredentials = await prisma.credentials.create({
|
||||
data: {
|
||||
...credentials,
|
||||
data: encryptedData,
|
||||
iv,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
return { credentialsId: createdCredentials.id }
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const deleteCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/credentials/:credentialsId',
|
||||
protect: true,
|
||||
summary: 'Delete credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Workspace not found',
|
||||
})
|
||||
|
||||
await prisma.credentials.delete({
|
||||
where: {
|
||||
id: credentialsId,
|
||||
},
|
||||
})
|
||||
return { credentialsId }
|
||||
}
|
||||
)
|
||||
55
apps/builder/src/features/credentials/api/listCredentials.ts
Normal file
55
apps/builder/src/features/credentials/api/listCredentials.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { stripeCredentialsSchema } from 'models/features/blocks/inputs/payment/schemas'
|
||||
import { googleSheetsCredentialsSchema } from 'models/features/blocks/integrations/googleSheets/schemas'
|
||||
import { openAICredentialsSchema } from 'models/features/blocks/integrations/openai'
|
||||
import { smtpCredentialsSchema } from 'models/features/blocks/integrations/sendEmail'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/credentials',
|
||||
protect: true,
|
||||
summary: 'List workspace credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
type: stripeCredentialsSchema.shape.type
|
||||
.or(smtpCredentialsSchema.shape.type)
|
||||
.or(googleSheetsCredentialsSchema.shape.type)
|
||||
.or(openAICredentialsSchema.shape.type),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentials: z.array(z.object({ id: z.string(), name: z.string() })),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
const credentials = await prisma.credentials.findMany({
|
||||
where: {
|
||||
type,
|
||||
workspaceId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
})
|
||||
return { credentials }
|
||||
})
|
||||
10
apps/builder/src/features/credentials/api/router.ts
Normal file
10
apps/builder/src/features/credentials/api/router.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { router } from '@/utils/server/trpc'
|
||||
import { createCredentials } from './createCredentials'
|
||||
import { deleteCredentials } from './deleteCredentials'
|
||||
import { listCredentials } from './listCredentials'
|
||||
|
||||
export const credentialsRouter = router({
|
||||
createCredentials,
|
||||
listCredentials,
|
||||
deleteCredentials,
|
||||
})
|
||||
@@ -10,58 +10,73 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { CredentialsType } from 'models'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '../../../hooks/useToast'
|
||||
import { deleteCredentialsQuery, useCredentials } from '@/features/credentials'
|
||||
import { Credentials } from 'models'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
type: CredentialsType
|
||||
type: Credentials['type']
|
||||
workspaceId: string
|
||||
currentCredentialsId?: string
|
||||
onCredentialsSelect: (credentialId?: string) => void
|
||||
onCreateNewClick: () => void
|
||||
defaultCredentialLabel?: string
|
||||
refreshDropdownKey?: number
|
||||
}
|
||||
|
||||
export const CredentialsDropdown = ({
|
||||
type,
|
||||
workspaceId,
|
||||
currentCredentialsId,
|
||||
onCredentialsSelect,
|
||||
onCreateNewClick,
|
||||
defaultCredentialLabel,
|
||||
refreshDropdownKey,
|
||||
...props
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const { credentials, mutate } = useCredentials({
|
||||
workspaceId: workspace?.id,
|
||||
const { data, refetch } = trpc.credentials.listCredentials.useQuery({
|
||||
workspaceId,
|
||||
type,
|
||||
})
|
||||
const [isDeleting, setIsDeleting] = useState<string>()
|
||||
const { mutate } = trpc.credentials.deleteCredentials.useMutation({
|
||||
onMutate: ({ credentialsId }) => {
|
||||
setIsDeleting(credentialsId)
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
description: error.message,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ credentialsId }) => {
|
||||
if (credentialsId === currentCredentialsId) onCredentialsSelect(undefined)
|
||||
refetch()
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsDeleting(undefined)
|
||||
},
|
||||
})
|
||||
|
||||
const defaultCredentialsLabel = defaultCredentialLabel ?? `Select an account`
|
||||
|
||||
const credentialsList = useMemo(() => {
|
||||
return credentials.filter((credential) => credential.type === type)
|
||||
}, [type, credentials])
|
||||
|
||||
const currentCredential = useMemo(
|
||||
() => credentials.find((c) => c.id === currentCredentialsId),
|
||||
[currentCredentialsId, credentials]
|
||||
const currentCredential = data?.credentials.find(
|
||||
(c) => c.id === currentCredentialsId
|
||||
)
|
||||
|
||||
const handleMenuItemClick = (credentialsId: string) => () => {
|
||||
onCredentialsSelect(credentialsId)
|
||||
}
|
||||
const handleMenuItemClick = useCallback(
|
||||
(credentialsId: string) => () => {
|
||||
onCredentialsSelect(credentialsId)
|
||||
},
|
||||
[onCredentialsSelect]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if ((refreshDropdownKey ?? 0) > 0) mutate({ credentials })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshDropdownKey])
|
||||
const clearQueryParams = useCallback(() => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@@ -69,29 +84,17 @@ export const CredentialsDropdown = ({
|
||||
handleMenuItemClick(router.query.credentialsId.toString())()
|
||||
clearQueryParams()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady])
|
||||
}, [
|
||||
clearQueryParams,
|
||||
handleMenuItemClick,
|
||||
router.isReady,
|
||||
router.query.credentialsId,
|
||||
])
|
||||
|
||||
const clearQueryParams = () => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}
|
||||
|
||||
const handleDeleteDomainClick =
|
||||
const deleteCredentials =
|
||||
(credentialsId: string) => async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!workspace?.id) return
|
||||
setIsDeleting(credentialsId)
|
||||
const { error } = await deleteCredentialsQuery(
|
||||
workspace.id,
|
||||
credentialsId
|
||||
)
|
||||
setIsDeleting(undefined)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
onCredentialsSelect(undefined)
|
||||
mutate({ credentials: credentials.filter((c) => c.id !== credentialsId) })
|
||||
mutate({ workspaceId, credentialsId })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -121,7 +124,7 @@ export const CredentialsDropdown = ({
|
||||
{defaultCredentialLabel}
|
||||
</MenuItem>
|
||||
)}
|
||||
{credentialsList.map((credentials) => (
|
||||
{data?.credentials.map((credentials) => (
|
||||
<MenuItem
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
@@ -137,7 +140,7 @@ export const CredentialsDropdown = ({
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove credentials"
|
||||
size="xs"
|
||||
onClick={handleDeleteDomainClick(credentials.id)}
|
||||
onClick={deleteCredentials(credentials.id)}
|
||||
isLoading={isDeleting === credentials.id}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const useCredentials = ({
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ credentials: Credentials[] }, Error>(
|
||||
workspaceId ? `/api/credentials?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
credentials: data?.credentials ?? [],
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1 @@
|
||||
export { CredentialsDropdown } from './components/CredentialsDropdown'
|
||||
export { useCredentials } from './hooks/useCredentials'
|
||||
export { createCredentialsQuery } from './queries/createCredentialsQuery'
|
||||
export { deleteCredentialsQuery } from './queries/deleteCredentialsQuery'
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const createCredentialsQuery = async (
|
||||
credentials: Omit<Credentials, 'id' | 'iv' | 'createdAt'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials?${stringify({
|
||||
workspaceId: credentials.workspaceId,
|
||||
})}`,
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Credentials } from 'db'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const deleteCredentialsQuery = async (
|
||||
workspaceId: string,
|
||||
credentialsId: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials/${credentialsId}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
Reference in New Issue
Block a user