Skip to content

Refactor: api key creation/retrieval/deletion to use infra api #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2 changes: 1 addition & 1 deletion src/features/dashboard/keys/create-api-key-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const CreateApiKeyDialog: FC<CreateApiKeyDialogProps> = ({
const { execute: createApiKey, isPending } = useAction(createApiKeyAction, {
onSuccess: ({ data }) => {
if (data?.createdApiKey) {
setCreatedApiKey(data.createdApiKey)
setCreatedApiKey(data.createdApiKey.key)
form.reset()
}
},
Expand Down
21 changes: 11 additions & 10 deletions src/features/dashboard/keys/table-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ export default async function TableBodyContent({
const result = await getTeamApiKeys({ teamId })

if (!result?.data || result.serverError || result.validationErrors) {
;<TableRow>
<TableCell colSpan={5}>
<ErrorIndicator
description={'Could not load API keys'}
message={result?.serverError || 'Unknown error'}
className="bg-bg mt-2 w-full max-w-full"
/>
</TableCell>
</TableRow>
return
return (
<TableRow>
<TableCell colSpan={5}>
<ErrorIndicator
description={'Could not load API keys'}
message={result?.serverError || 'Unknown error'}
className="bg-bg mt-2 w-full max-w-full"
/>
</TableCell>
</TableRow>
)
}

const { apiKeys } = result.data
Expand Down
14 changes: 9 additions & 5 deletions src/features/dashboard/keys/table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/ui/primitives/dropdown-menu'
import { ObscuredApiKey } from '@/server/keys/types'
import { deleteApiKeyAction } from '@/server/keys/key-actions'
import { AlertDialog } from '@/ui/alert-dialog'
import { useState } from 'react'
Expand All @@ -21,9 +20,10 @@ import { motion } from 'motion/react'
import { exponentialSmoothing } from '@/lib/utils'
import { useAction } from 'next-safe-action/hooks'
import { defaultSuccessToast, defaultErrorToast } from '@/lib/hooks/use-toast'
import { TeamAPIKey } from '@/types/api'

interface TableRowProps {
apiKey: ObscuredApiKey
apiKey: TeamAPIKey
index: number
}

Expand Down Expand Up @@ -63,6 +63,8 @@ export default function ApiKeyTableRow({ apiKey, index }: TableRowProps) {
})
}

const concatedKeyMask = `${apiKey.mask.prefix}${apiKey.mask.maskedValuePrefix}......${apiKey.mask.maskedValueSuffix}`

return (
<>
<AlertDialog
Expand All @@ -83,12 +85,14 @@ export default function ApiKeyTableRow({ apiKey, index }: TableRowProps) {
onMouseEnter={() => setHoveredRowIndex(index)}
onMouseLeave={() => setHoveredRowIndex(-1)}
>
<TableCell className="flex flex-col gap-1 text-left font-mono">
<TableCell className="text-lef flex flex-col gap-1">
{apiKey.name}
<span className="text-fg-500 pl-1">{apiKey.maskedKey}</span>
<span className="text-fg-500 pl-0.25 font-mono text-xs">
{concatedKeyMask}
</span>
</TableCell>
<TableCell className="text-fg-500 max-w-36 truncate overflow-hidden">
<span className="max-w-full truncate">{apiKey.createdBy}</span>
<span className="max-w-full truncate">{apiKey.createdBy?.email}</span>
</TableCell>
<TableCell className="text-fg-300 text-right">
{apiKey.createdAt
Expand Down
60 changes: 24 additions & 36 deletions src/lib/clients/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ const BLACKLISTED_INPUT_KEYS = [
'secret',
'token',
'apiKey',
'key',
]

function sanitizeObject(data: unknown, blacklist: string[]): unknown {
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
const sanitized = { ...(data as Record<string, unknown>) }
for (const key in sanitized) {
if (blacklist.includes(key)) {
sanitized[key] = '[REDACTED]'
} else {
sanitized[key] = sanitizeObject(sanitized[key], blacklist)
}
}
return sanitized
} else if (Array.isArray(data)) {
return data.map((item) => sanitizeObject(item, blacklist))
}
return data
}

export const actionClient = createSafeActionClient({
handleServerError(e) {
if (e instanceof ActionError) {
Expand Down Expand Up @@ -60,54 +78,24 @@ export const actionClient = createSafeActionClient({

// filter out blacklisted keys from clientInput for logging
let sanitizedInput: unknown = clientInput
sanitizedInput = sanitizeObject(clientInput, BLACKLISTED_INPUT_KEYS)

// handle object case
if (
typeof clientInput === 'object' &&
clientInput !== null &&
!Array.isArray(clientInput)
) {
sanitizedInput = { ...(clientInput as Record<string, unknown>) }
const sanitizedObj = sanitizedInput as Record<string, unknown>

for (const key of BLACKLISTED_INPUT_KEYS) {
if (key in sanitizedObj) {
sanitizedObj[key] = '[REDACTED]'
}
}
}
// handle array case
else if (Array.isArray(clientInput)) {
sanitizedInput = [...clientInput]
const sanitizedArray = sanitizedInput as unknown[]

// check if any array elements are objects that need sanitizing
for (let i = 0; i < sanitizedArray.length; i++) {
const item = sanitizedArray[i]
if (typeof item === 'object' && item !== null) {
const sanitizedItem = { ...(item as Record<string, unknown>) }
for (const key of BLACKLISTED_INPUT_KEYS) {
if (key in sanitizedItem) {
sanitizedItem[key] = '[REDACTED]'
}
}
sanitizedArray[i] = sanitizedItem
}
}
}
// Sanitize result object
let sanitizedRest: unknown = rest
sanitizedRest = sanitizeObject(rest, BLACKLISTED_INPUT_KEYS)

if (
result.serverError ||
result.validationErrors ||
result.success === false
) {
logError(`${actionOrFunction} '${actionOrFunctionName}' failed:`, {
result: rest,
result: sanitizedRest,
input: sanitizedInput,
})
} else if (VERBOSE) {
logSuccess(`${actionOrFunction} '${actionOrFunctionName}' succeeded:`, {
result: rest,
result: sanitizedRest,
input: sanitizedInput,
})
}
Expand Down
61 changes: 23 additions & 38 deletions src/server/keys/get-api-keys.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'server-only'

import { z } from 'zod'
import { supabaseAdmin } from '@/lib/clients/supabase/admin'
import { maskApiKey } from '@/lib/utils/server'
import { checkUserTeamAuthorization } from '@/lib/utils/server'
import { ObscuredApiKey } from './types'
import { getApiUrl } from '@/lib/utils/server'
import { authActionClient } from '@/lib/clients/action'
import { returnServerError } from '@/lib/utils/action'
import { SUPABASE_AUTH_HEADERS } from '@/configs/constants'
import { logError } from '@/lib/clients/logger'
import { ERROR_CODES } from '@/configs/logs'
import { TeamAPIKey } from '@/types/api'

const GetApiKeysSchema = z.object({
teamId: z.string({ required_error: 'Team ID is required' }).uuid(),
Expand All @@ -17,45 +18,29 @@ export const getTeamApiKeys = authActionClient
.metadata({ serverFunctionName: 'getTeamApiKeys' })
.action(async ({ parsedInput, ctx }) => {
const { teamId } = parsedInput
const { user } = ctx
const { session } = ctx

const isAuthorized = await checkUserTeamAuthorization(user.id, teamId)
const accessToken = session.access_token

if (!isAuthorized)
return returnServerError('Not authorized to edit team api keys')
const { url } = await getApiUrl()

const { data, error } = await supabaseAdmin
.from('team_api_keys')
.select('*')
.eq('team_id', teamId)
.order('created_at', { ascending: true })
const response = await fetch(`${url}/api-keys`, {
headers: {
'Content-Type': 'application/json',
...SUPABASE_AUTH_HEADERS(accessToken, teamId),
},
})

if (error) throw error

const resultApiKeys: ObscuredApiKey[] = []

for (const apiKey of data) {
let userEmail: string | null = null

if (apiKey.created_by) {
const { data: keyUserData } = await supabaseAdmin
.from('auth_users')
.select('email')
.eq('id', apiKey.created_by)

if (keyUserData && keyUserData[0]) {
userEmail = keyUserData[0].email
}
}

resultApiKeys.push({
id: apiKey.id,
name: apiKey.name,
maskedKey: maskApiKey(apiKey),
createdAt: apiKey.created_at,
createdBy: userEmail,
if (!response.ok) {
const text = await response.text()
logError(ERROR_CODES.INFRA, 'Failed to get api keys', {
teamId,
error: text,
})
return returnServerError('Failed to get api keys')
}

return { apiKeys: resultApiKeys }
const data = (await response.json()) as TeamAPIKey[]

return { apiKeys: data }
})
Loading