Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions apps/api-backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ import { Gemini } from "./llms/Gemini";
import { OpenAi } from "./llms/Openai";
import { Claude } from "./llms/Claude";
import { LlmResponse } from "./llms/Base";
import { createHash } from "crypto";

/**
* Hash an API key using SHA-256
* This must match the hashing logic in the primary-backend service
*/
function hashApiKey(rawApiKey: string): string {
return createHash("sha256").update(rawApiKey).digest("hex");
}

const app = new Elysia()
.use(bearer())
.use(openapi());
.post("/api/v1/chat/completions", async ({ status, bearer: apiKey, body }) => {
const model = body.model;
const [_companyName, providerModelName] = model.split("/");

// Validate API key by hashing and querying
const apiKeyHash = hashApiKey(apiKey);
const apiKeyDb = await prisma.apiKey.findFirst({
where: {
apiKey,
apiKeyHash,
disabled: false,
deleted: false
},
select: {
id: true,
user: true
}
})
Expand Down Expand Up @@ -67,19 +80,19 @@ const app = new Elysia()
if (provider.provider.name === "Google Vertex") {
response = await Gemini.chat(providerModelName, body.messages)
}

if (provider.provider.name === "OpenAI") {
response = await OpenAi.chat(providerModelName, body.messages)
}

if (provider.provider.name === "Claude API") {
response = await Claude.chat(providerModelName, body.messages)
}

if (!response) {
return status(403, {
message: "No provider found for this model"
})
})
}

const creditsUsed = (response.inputTokensConsumed * provider.inputTokenCost + response.outputTokensConsumed * provider.outputTokenCost) / 10;
Expand All @@ -97,12 +110,13 @@ const app = new Elysia()
console.log(res)
const res2 = await prisma.apiKey.update({
where: {
apiKey: apiKey
},
apiKeyHash: apiKeyHash
},
data: {
creditsConsumed: {
increment: creditsUsed
}
},
lastUsed: new Date()
}
})
console.log(res2)
Expand Down
46 changes: 0 additions & 46 deletions apps/dashboard-frontend/src/pages/ApiKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import {
ToggleLeft,
ToggleRight,
Key,
Eye,
EyeOff,
} from "lucide-react";

export function ApiKeys() {
Expand All @@ -32,7 +30,6 @@ export function ApiKeys() {
const nameRef = useRef<HTMLInputElement>(null);
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [revealedKeys, setRevealedKeys] = useState<Set<string>>(new Set());

const apiKeysQuery = useQuery({
queryKey: ["api-keys"],
Expand Down Expand Up @@ -93,15 +90,6 @@ export function ApiKeys() {
setTimeout(() => setCopiedId(null), 2000);
};

const toggleReveal = (id: string) => {
setRevealedKeys((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};

const apiKeys = apiKeysQuery.data?.apiKeys ?? [];

return (
Expand Down Expand Up @@ -229,7 +217,6 @@ export function ApiKeys() {
<thead>
<tr className="border-b border-border/50">
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground">Name</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground">Key</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground">Status</th>
<th className="text-right px-4 py-3 text-xs font-medium text-muted-foreground">Credits Used</th>
<th className="text-right px-4 py-3 text-xs font-medium text-muted-foreground">Actions</th>
Expand All @@ -239,39 +226,6 @@ export function ApiKeys() {
{apiKeys.map((key) => (
<tr key={key.id} className="border-b border-border/30 last:border-0 group">
<td className="px-4 py-3 font-medium">{key.name}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1.5">
<code className="font-mono text-xs text-muted-foreground">
{revealedKeys.has(key.id)
? key.apiKey
: `${key.apiKey.slice(0, 12)}${"•".repeat(8)}`}
</code>
<Button
variant="ghost"
size="icon-sm"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => toggleReveal(key.id)}
>
{revealedKeys.has(key.id) ? (
<EyeOff className="size-3" />
) : (
<Eye className="size-3" />
)}
</Button>
<Button
variant="ghost"
size="icon-sm"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copyToClipboard(key.apiKey, key.id)}
>
{copiedId === key.id ? (
<CheckCircle2 className="size-3 text-emerald-400" />
) : (
<Copy className="size-3" />
)}
</Button>
</div>
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium ${
Expand Down
Loading