Skip to content

Commit

Permalink
feat: create, show, and delete API Keys
Browse files Browse the repository at this point in the history
  • Loading branch information
poswalsameer committed Feb 9, 2025
1 parent 2c74f03 commit fe320d4
Show file tree
Hide file tree
Showing 8 changed files with 495 additions and 8 deletions.
67 changes: 62 additions & 5 deletions apps/platform/src/app/(main)/(settings)/settings/@profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
'use client'
import React, { useCallback, useState } from 'react'
import React, { useCallback, useState, useEffect } from 'react'
import { toast } from 'sonner'
import { useAtom } from 'jotai'
import { useAtom, useAtomValue } from 'jotai'
import InputLoading from './loading'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import ControllerInstance from '@/lib/controller-instance'
import { Button } from '@/components/ui/button'
import { userAtom } from '@/store'
import {
userAtom,
apiKeysOfProjectAtom,
deleteApiKeyOpenAtom,
selectedApiKeyAtom
} from '@/store'
import { useSearchParams } from 'next/navigation'
import AddApiKeyDialog from '@/components/userProfile/apiKeys/addApiKeyDialog'
import ApiKeyCard from '@/components/userProfile/apiKeys/apiKeyCard'
import ConfirmDeleteApiKey from '@/components/userProfile/apiKeys/confirmDeleteApiKey'

function ProfilePage(): React.JSX.Element {
const [user, setUser] = useAtom(userAtom)
const [apiKeys, setApiKeys] = useAtom(apiKeysOfProjectAtom)
const isDeleteApiKeyOpen = useAtomValue(deleteApiKeyOpenAtom)
const selectedApiKey = useAtomValue(selectedApiKeyAtom)

const [isLoading, setIsLoading] = useState<boolean>(true)
const [userData, setUserData] = useState({
Expand All @@ -20,6 +32,9 @@ function ProfilePage(): React.JSX.Element {
})
const [isModified, setIsModified] = useState<boolean>(false)

const searchParams = useSearchParams()
const tab = searchParams.get('profile') ?? 'profile'

const updateSelf = useCallback(async () => {
toast.loading('Updating profile...')
setIsLoading(true)
Expand Down Expand Up @@ -63,6 +78,33 @@ function ProfilePage(): React.JSX.Element {
setIsLoading(false)
}, [userData.name, userData.email, user?.name, user?.email, setUser])

useEffect(() => {
const getAllApiKeys = async () => {
const { success, error, data } = await ControllerInstance.getInstance().apiKeyController.getApiKeysOfUser(
{},
{}
)

if (success && data) {
setApiKeys(data.items)
}
if (error) {
toast.error('Something went wrong!', {
description: (
<p className="text-xs text-red-300">
Something went wrong while fetching API Keys. Check console for
more info.
</p>
)
})
// eslint-disable-next-line no-console -- we need to log the error
console.error(error)
}
}

getAllApiKeys()
}, [setApiKeys])

return (
<main className="flex flex-col gap-y-10">
{/* Avatar */}
Expand Down Expand Up @@ -124,15 +166,30 @@ function ProfilePage(): React.JSX.Element {
Save Changes
</Button>
</div>
<Separator className="max-w-[30vw] bg-white/15" />
<div className="flex max-w-[20vw] flex-col gap-4">
<Separator className="w-full bg-white/15" />
<div className="flex flex-row justify-between items-center gap-4 p-3">
<div className="flex flex-col gap-2">
<div className="text-xl font-semibold">API Keys</div>
<span className="text-sm text-white/70">
Generate new API keys to use with the Keyshade CLI.
</span>
</div>
<div>
{tab === 'profile' && <AddApiKeyDialog />}
</div>
</div>
{apiKeys.length !== 0 &&
<div className={`grid h-fit w-full grid-cols-1 gap-8 p-3 text-white md:grid-cols-2 xl:grid-cols-3 `}>
{apiKeys.map((apiKey) => (
<ApiKeyCard apiKey={apiKey} key={apiKey.id} />
))}

{/* Delete API Key alert dialog */}
{isDeleteApiKeyOpen && selectedApiKey ? (
<ConfirmDeleteApiKey />
) : null}
</div>
}

<Separator className="max-w-[30vw] bg-white/15" />

Expand Down
2 changes: 1 addition & 1 deletion apps/platform/src/components/common/slug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function Slug({ text }: SlugProps): React.JSX.Element {

return (
<button
className={`${roboto.className} flex cursor-copy gap-2 rounded-lg bg-white/10 px-3 py-2 font-mono text-sm text-white/50 hover:bg-white/15`}
className={`${roboto.className} flex justify-between cursor-copy gap-2 rounded-lg bg-white/10 px-3 py-2 font-mono text-sm text-white/50 hover:bg-white/15`}
onClick={copyToClipboard}
type="button"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import React, { useCallback, useState } from 'react'
import { AddSVG } from '@public/svg/shared'
import type { CreateApiKeyRequest } from '@keyshade/schema'
import { toast } from 'sonner'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from '../../../ui/dialog'
import { Button } from '../../../ui/button'
import { Input } from '../../../ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '../../../ui/select'
import ControllerInstance from '@/lib/controller-instance'
import {
createApiKeyOpenAtom,
apiKeysOfProjectAtom
} from '@/store'

export default function AddApiKeyDialog() {
const [isLoading, setIsLoading] = useState(false)
const [isCreateApiKeyOpen, setIsCreateApiKeyOpen] = useAtom(createApiKeyOpenAtom)
const setApiKeys = useSetAtom(apiKeysOfProjectAtom)

const [newApiKeyData, setNewApiKeyData] = useState({
apiKeyName: '',
expiryDate: '24'
})

const handleAddApiKey = useCallback(async () => {
setIsLoading(true)

if (!newApiKeyData.apiKeyName) {
toast.error('API Key name is required')
return
}
if (!newApiKeyData.expiryDate) {
toast.error('Expiry Date is required')
return
}

const request: CreateApiKeyRequest = {
name: newApiKeyData.apiKeyName,
expiresAfter: newApiKeyData.expiryDate as "never" | "24" | "168" | "720" | "8760" | undefined
}

toast.loading("Creating your API Key...")
const { success, error, data } = await ControllerInstance.getInstance().apiKeyController.crateApiKey(
request,
{}
)

toast.dismiss()
if (success && data) {
toast.success('API Key added successfully', {
description: (
<p className="text-xs text-emerald-300">You created a new API Key</p>
)
})
// Add the new API Key to the list of keys
setApiKeys((prev) => [...prev, data])
}
if (error) {
toast.dismiss()
if (error.statusCode === 409) {
toast.error('API Key already exists', {
description: (
<p className="text-xs text-red-300">
An API Key with the same name already exists. Please use a different one.
</p>
)
})
} else {
toast.error('Something went wrong!', {
description: (
<p className="text-xs text-red-300">
Something went wrong while adding the API Key. Check the console for more details.
</p>
)
})
// eslint-disable-next-line no-console -- we need to log the error that are not in the if condition
console.error(error)
}
}

setNewApiKeyData({
apiKeyName: '',
expiryDate: ''
})
setIsLoading(false)
setIsCreateApiKeyOpen(false)
}, [newApiKeyData, setIsCreateApiKeyOpen, setApiKeys])

return (
<Dialog
onOpenChange={() => setIsCreateApiKeyOpen(!isCreateApiKeyOpen)}
open={isCreateApiKeyOpen}
>
<DialogTrigger asChild>
<Button
className="bg-[#26282C] hover:bg-[#161819] hover:text-white/55"
variant="outline"
>
<AddSVG /> Add API Key
</Button>
</DialogTrigger>
<DialogContent className=" w-[31.625rem] bg-[#18181B] text-white ">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold">
Add a new API Key
</DialogTitle>
<DialogDescription>
Add a new API key to the project
</DialogDescription>
</DialogHeader>

<div className=" text-white">
<div className="space-y-4">
<div className="flex h-[2.75rem] w-[28.625rem] items-center justify-center gap-6">
<label
className="h-[1.25rem] w-[7.125rem] text-base font-semibold"
htmlFor="secret-name"
>
API Key Name
</label>
<Input
className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500"
id="secret-name"
onChange={(e) =>
setNewApiKeyData({
...newApiKeyData,
apiKeyName: e.target.value
})
}
placeholder="Enter the API key"
value={newApiKeyData.apiKeyName}
/>
</div>

<div className="flex h-[2.75rem] w-[28.625rem] items-center justify-center gap-6">
<label
className="h-[1.25rem] w-[7.125rem] text-base font-semibold"
htmlFor="secrete-note"
>
Expiry Date
</label>
<Select
defaultValue="24"
onValueChange={(val) =>
setNewApiKeyData({
...newApiKeyData,
expiryDate: val
})
}
>
<SelectTrigger className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent className=" border border-white/10 bg-neutral-800 text-gray-300">
<SelectItem value="24"> 1 day </SelectItem>
<SelectItem value="168"> 1 week </SelectItem>
<SelectItem value="720"> 1 month </SelectItem>
<SelectItem value="8760"> 1 year </SelectItem>
<SelectItem value="never"> Never </SelectItem>
</SelectContent>
</Select>
</div>

<div className="flex justify-end pt-4">
<Button
className="h-[2.625rem] w-[6.25rem] rounded-lg bg-white text-xs font-semibold text-black hover:bg-gray-200"
onClick={handleAddApiKey}
disabled={isLoading}
>
Add API Key
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client'

import type { ApiKey } from '@keyshade/schema'
import dayjs from 'dayjs'
import { useSetAtom } from 'jotai'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import {
selectedApiKeyAtom,
editApiKeyOpenAtom,
deleteApiKeyOpenAtom
} from '@/store'
import Slug from '@/components/common/slug'

const formatDate = (date: string): string => {
return dayjs(date).format('D MMMM, YYYY')
}

export default function ApiKeyCard({
apiKey
}: {
apiKey: ApiKey
}): React.JSX.Element {
const setSelectedApiKey = useSetAtom(selectedApiKeyAtom)
const setIsEditApiKeyOpen = useSetAtom(editApiKeyOpenAtom)
const setIsDeleteApiKeyOpen = useSetAtom(deleteApiKeyOpenAtom)

const handleDeleteClick = () => {
setSelectedApiKey(apiKey)
setIsDeleteApiKeyOpen(true)
}

return (
<ContextMenu key={apiKey.id}>
<ContextMenuTrigger className="w-full hover:cursor-pointer">
<div className="flex h-fit flex-col rounded-xl border-[1px] border-white/20 bg-white/[2%] transition-all duration-150 ease-in hover:bg-white/[5%]">
<div className="flex flex-col gap-y-2 px-6 py-4">
<div className="flex w-full flex-row items-center justify-between">
<div className="text-xl"> {apiKey.name} </div>
</div>
</div>
<div className="flex flex-row justify-between items-end rounded-b-xl bg-white/[6%] px-6 py-4 text-sm text-white/50">
<div className="w-1/2 flex flex-col">
<Slug text={apiKey.slug} />
</div>
<div className="flex flex-col items-end">
<div>Expiring in</div>
<div>
{apiKey.expiresAt ? (
dayjs(apiKey.expiresAt).diff(dayjs(), "day") >= 1 ? (
`${dayjs(apiKey.expiresAt).diff(dayjs(), "day")} days`
) : (
`${dayjs(apiKey.expiresAt).diff(dayjs(), "hour")} hours`
)
) : "Never"}
</div>
</div>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="flex w-[15.938rem] flex-col items-center justify-center rounded-lg bg-[#3F3F46]">
<ContextMenuItem
className="h-[33%] w-[15.938rem] text-xs font-semibold tracking-wide"
>
Edit
</ContextMenuItem>
<ContextMenuItem
className="h-[33%] w-[15.938rem] text-xs font-semibold tracking-wide"
onSelect={handleDeleteClick}
>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
Loading

0 comments on commit fe320d4

Please sign in to comment.