ddd refactor: api-keys

This commit is contained in:
Ramnique Singh 2025-08-15 12:48:10 +05:30
parent 8488255d6d
commit d4f0db1f09
15 changed files with 377 additions and 196 deletions

View file

@ -8,17 +8,22 @@ import { revalidatePath } from "next/cache";
import { templates } from "../lib/project_templates";
import { authCheck } from "./auth_actions";
import { User, WithStringId } from "../lib/types/types";
import { ApiKey } from "../lib/types/project_types";
import { ApiKey } from "@/src/entities/models/api-key";
import { Project } from "../lib/types/project_types";
import { USE_AUTH } from "../lib/feature_flags";
import { authorizeUserAction } from "./billing_actions";
import { Workflow } from "../lib/types/workflow_types";
import { container } from "@/di/container";
import { IProjectActionAuthorizationPolicy } from "@/src/application/policies/project-action-authorization.policy";
import { ICreateApiKeyController } from "@/src/interface-adapters/controllers/api-keys/create-api-key.controller";
import { IListApiKeysController } from "@/src/interface-adapters/controllers/api-keys/list-api-keys.controller";
import { IDeleteApiKeyController } from "@/src/interface-adapters/controllers/api-keys/delete-api-key.controller";
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
const projectActionAuthorizationPolicy = container.resolve<IProjectActionAuthorizationPolicy>('projectActionAuthorizationPolicy');
const createApiKeyController = container.resolve<ICreateApiKeyController>('createApiKeyController');
const listApiKeysController = container.resolve<IListApiKeysController>('listApiKeysController');
const deleteApiKeyController = container.resolve<IDeleteApiKeyController>('deleteApiKeyController');
export async function listTemplates() {
const templatesArray = Object.entries(templates)
@ -180,36 +185,32 @@ export async function updateWebhookUrl(projectId: string, url: string) {
);
}
export async function createApiKey(projectId: string): Promise<WithStringId<z.infer<typeof ApiKey>>> {
await projectAuthCheck(projectId);
// count existing keys
const count = await apiKeysCollection.countDocuments({ projectId });
if (count >= 3) {
throw new Error('Maximum number of API keys reached');
}
// create key
const key = crypto.randomBytes(32).toString('hex');
const doc: z.infer<typeof ApiKey> = {
export async function createApiKey(projectId: string): Promise<z.infer<typeof ApiKey>> {
const user = await authCheck();
return await createApiKeyController.execute({
caller: 'user',
userId: user._id,
projectId,
key,
createdAt: new Date().toISOString(),
};
await apiKeysCollection.insertOne(doc);
const { _id, ...rest } = doc as WithStringId<z.infer<typeof ApiKey>>;
return { ...rest, _id: _id.toString() };
});
}
export async function deleteApiKey(projectId: string, id: string) {
await projectAuthCheck(projectId);
await apiKeysCollection.deleteOne({ projectId, _id: new ObjectId(id) });
const user = await authCheck();
return await deleteApiKeyController.execute({
caller: 'user',
userId: user._id,
projectId,
id,
});
}
export async function listApiKeys(projectId: string): Promise<WithStringId<z.infer<typeof ApiKey>>[]> {
await projectAuthCheck(projectId);
const keys = await apiKeysCollection.find({ projectId }).toArray();
return keys.map(k => ({ ...k, _id: k._id.toString() }));
export async function listApiKeys(projectId: string): Promise<z.infer<typeof ApiKey>[]> {
const user = await authCheck();
return await listApiKeysController.execute({
caller: 'user',
userId: user._id,
projectId,
});
}
export async function updateProjectName(projectId: string, name: string) {

View file

@ -1,7 +1,7 @@
import { MongoClient } from "mongodb";
import { User, Webpage } from "./types/types";
import { Workflow } from "./types/workflow_types";
import { ApiKey } from "./types/project_types";
import { ApiKey } from "@/src/entities/models/api-key";
import { ProjectMember } from "./types/project_types";
import { Project } from "./types/project_types";
import { EmbeddingDoc } from "./types/datasource_types";

View file

@ -41,11 +41,4 @@ export const ProjectMember = z.object({
projectId: z.string(),
createdAt: z.string().datetime(),
lastUpdatedAt: z.string().datetime(),
});
export const ApiKey = z.object({
projectId: z.string(),
key: z.string(),
createdAt: z.string().datetime(),
lastUsedAt: z.string().datetime().optional(),
});

View file

@ -4,14 +4,10 @@ import { Metadata } from "next";
import { Spinner, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider, Textarea } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { ReactNode, useEffect, useState } from "react";
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
import { getProjectConfig, updateProjectName, updateWebhookUrl, deleteProject, rotateSecret } from "../../../actions/project_actions";
import { CopyButton } from "../../../../components/common/copy-button";
import { InputField } from "../../../lib/components/input-field";
import { EyeIcon, EyeOffIcon, Settings, Plus, MoreVertical } from "lucide-react";
import { WithStringId } from "../../../lib/types/types";
import { ApiKey } from "../../../lib/types/project_types";
import { z } from "zod";
import { RelativeTime } from "@primer/react";
import { Label } from "../../../lib/components/label";
import { FormSection } from "../../../lib/components/form-section";
import { Panel } from "@/components/common/panel-common";
@ -108,156 +104,6 @@ export function BasicSettingsSection({
</Section>;
}
export function ApiKeysSection({
projectId,
}: {
projectId: string;
}) {
const [keys, setKeys] = useState<WithStringId<z.infer<typeof ApiKey>>[]>([]);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<{
type: 'success' | 'error' | 'info';
text: string;
} | null>(null);
useEffect(() => {
const loadKeys = async () => {
const keys = await listApiKeys(projectId);
setKeys(keys);
setLoading(false);
};
loadKeys();
}, [projectId]);
const handleCreateKey = async () => {
setLoading(true);
setMessage(null);
try {
const key = await createApiKey(projectId);
setLoading(false);
setMessage({
type: 'success',
text: 'API key created successfully',
});
setKeys([...keys, key]);
setTimeout(() => {
setMessage(null);
}, 2000);
} catch (error) {
setLoading(false);
setMessage({
type: 'error',
text: error instanceof Error ? error.message : "Failed to create API key",
});
}
};
const handleDeleteKey = async (id: string) => {
if (!window.confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
return;
}
try {
setLoading(true);
setMessage(null);
await deleteApiKey(projectId, id);
setKeys(keys.filter((k) => k._id !== id));
setLoading(false);
setMessage({
type: 'info',
text: 'API key deleted successfully',
});
setTimeout(() => {
setMessage(null);
}, 2000);
} catch (error) {
setLoading(false);
setMessage({
type: 'error',
text: error instanceof Error ? error.message : "Failed to delete API key",
});
}
};
return <Section title="API keys">
<div className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground">
API keys are used to authenticate requests to the Rowboat API.
</p>
<Button
onClick={handleCreateKey}
size="sm"
startContent={<Plus className="h-4 w-4" />}
variant="primary"
disabled={loading}
>
Create API key
</Button>
</div>
<Divider />
{loading && <Spinner size="sm" />}
{!loading && <div className="border border rounded-lg text-sm">
<div className="flex items-center border-b border p-4">
<div className="flex-3 font-normal">API Key</div>
<div className="flex-1 font-normal">Created</div>
<div className="flex-1 font-normal">Last Used</div>
<div className="w-10"></div>
</div>
{message?.type === 'success' && <div className="flex flex-col p-2">
<div className="text-sm bg-green-50 text-green-500 p-2 rounded-md">{message.text}</div>
</div>}
{message?.type === 'error' && <div className="flex flex-col p-2">
<div className="text-sm bg-red-50 text-red-500 p-2 rounded-md">{message.text}</div>
</div>}
{message?.type === 'info' && <div className="flex flex-col p-2">
<div className="text-sm bg-yellow-50 text-yellow-500 p-2 rounded-md">{message.text}</div>
</div>}
<div className="flex flex-col">
{keys.map((key) => (
<div key={key._id} className="flex items-start border-b border last:border-b-0 p-4">
<div className="flex-3 p-2">
<ApiKeyDisplay apiKey={key.key} />
</div>
<div className="flex-1 p-2">
<RelativeTime date={new Date(key.createdAt)} />
</div>
<div className="flex-1 p-2">
{key.lastUsedAt ? <RelativeTime date={new Date(key.lastUsedAt)} /> : 'Never'}
</div>
<div className="w-10 p-2">
<Dropdown>
<DropdownTrigger>
<button className="text-muted-foreground hover:text-foreground">
<MoreVertical className="h-4 w-4" />
</button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem
key='delete'
className="text-destructive"
onPress={() => handleDeleteKey(key._id)}
>
Delete
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
))}
{keys.length === 0 && (
<div className="p-4 text-center text-muted-foreground">
No API keys created yet
</div>
)}
</div>
</div>}
</div>
</Section>;
}
export function SecretSection({
projectId,
}: {

View file

@ -7,7 +7,7 @@ import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProjec
import { CopyButton } from "../../../../../components/common/copy-button";
import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { WithStringId } from "../../../../lib/types/types";
import { ApiKey } from "../../../../lib/types/project_types";
import { ApiKey } from "@/src/entities/models/api-key";
import { z } from "zod";
import { RelativeTime } from "@primer/react";
import { Label } from "../../../../lib/components/label";
@ -224,7 +224,7 @@ function ApiKeyDisplay({ apiKey, onDelete }: { apiKey: string; onDelete: () => v
}
function ApiKeysSection({ projectId }: { projectId: string }) {
const [keys, setKeys] = useState<WithStringId<z.infer<typeof ApiKey>>[]>([]);
const [keys, setKeys] = useState<z.infer<typeof ApiKey>[]>([]);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<{
type: 'success' | 'error' | 'info';
@ -270,7 +270,7 @@ function ApiKeysSection({ projectId }: { projectId: string }) {
try {
setLoading(true);
await deleteApiKey(projectId, id);
setKeys(keys.filter((k) => k._id !== id));
setKeys(keys.filter((k) => k.id !== id));
setMessage({
type: 'info',
text: 'API key deleted successfully',
@ -325,11 +325,11 @@ function ApiKeysSection({ projectId }: { projectId: string }) {
)}
{keys.map((key) => (
<div key={key._id} className="grid grid-cols-12 items-center border-b border-gray-200 dark:border-gray-700 last:border-0 p-4">
<div key={key.id} className="grid grid-cols-12 items-center border-b border-gray-200 dark:border-gray-700 last:border-0 p-4">
<div className="col-span-7">
<ApiKeyDisplay
apiKey={key.key}
onDelete={() => handleDeleteKey(key._id)}
onDelete={() => handleDeleteKey(key.id)}
/>
</div>
<div className="col-span-3 text-sm text-gray-500">