Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ erDiagram
string name
string key
string machineId
boolean isActive
}

USAGE_ENTRY {
Expand Down
50 changes: 42 additions & 8 deletions src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components";
import { Card, Button, Input, Modal, CardSkeleton, Toggle } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";

const DEFAULT_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || "";
Expand Down Expand Up @@ -257,6 +257,21 @@ export default function APIPageClient({ machineId }) {
}
};

const handleToggleKey = async (id, isActive) => {
try {
const res = await fetch(`/api/keys/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive }),
});
if (res.ok) {
setKeys(prev => prev.map(k => k.id === id ? { ...k, isActive } : k));
}
} catch (error) {
console.log("Error toggling key:", error);
}
};

const [baseUrl, setBaseUrl] = useState("/v1");
const cloudEndpointNew = cloudUrl ? `${cloudUrl}/v1` : "";

Expand Down Expand Up @@ -377,7 +392,7 @@ export default function APIPageClient({ machineId }) {
{keys.map((key) => (
<div
key={key.id}
className="group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0"
className={`group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${key.isActive === false ? "opacity-60" : ""}`}
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{key.name}</p>
Expand All @@ -395,13 +410,32 @@ export default function APIPageClient({ machineId }) {
<p className="text-xs text-text-muted mt-1">
Created {new Date(key.createdAt).toLocaleDateString()}
</p>
{key.isActive === false && (
<p className="text-xs text-orange-500 mt-1">Paused</p>
)}
</div>
<div className="flex items-center gap-2">
<Toggle
size="sm"
checked={key.isActive ?? true}
onChange={(checked) => {
if (key.isActive && !checked) {
if (confirm(`Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`)) {
handleToggleKey(key.id, checked);
}
} else {
handleToggleKey(key.id, checked);
}
}}
title={key.isActive ? "Pause key" : "Resume key"}
/>
<button
onClick={() => handleDeleteKey(key.id)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-0 group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
<button
onClick={() => handleDeleteKey(key.id)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-0 group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
))}
</div>
Expand Down
42 changes: 41 additions & 1 deletion src/app/api/keys/[id]/route.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
import { NextResponse } from "next/server";
import { deleteApiKey, isCloudEnabled } from "@/lib/localDb";
import { deleteApiKey, getApiKeyById, updateApiKey, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";

// GET /api/keys/[id] - Get single key
export async function GET(request, { params }) {
try {
const { id } = await params;
const key = await getApiKeyById(id);
if (!key) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
return NextResponse.json({ key });
} catch (error) {
console.log("Error fetching key:", error);
return NextResponse.json({ error: "Failed to fetch key" }, { status: 500 });
}
}

// PUT /api/keys/[id] - Update key
export async function PUT(request, { params }) {
try {
const { id } = await params;
const body = await request.json();
const { isActive } = body;

const existing = await getApiKeyById(id);
if (!existing) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}

const updateData = {};
if (isActive !== undefined) updateData.isActive = isActive;

const updated = await updateApiKey(id, updateData);
await syncKeysToCloudIfEnabled();

return NextResponse.json({ key: updated });
} catch (error) {
console.log("Error updating key:", error);
return NextResponse.json({ error: "Failed to update key" }, { status: 500 });
}
}

// DELETE /api/keys/[id] - Delete API key
export async function DELETE(request, { params }) {
try {
Expand Down
37 changes: 36 additions & 1 deletion src/lib/localDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ function ensureDbShape(data) {
}
}
}

// Migrate existing API keys to have isActive
if (key === "apiKeys" && Array.isArray(next.apiKeys)) {
for (const apiKey of next.apiKeys) {
if (apiKey.isActive === undefined || apiKey.isActive === null) {
apiKey.isActive = true;
changed = true;
}
}
}
}

return { data: next, changed };
Expand Down Expand Up @@ -649,6 +659,7 @@ export async function createApiKey(name, machineId) {
name: name,
key: result.key,
machineId: machineId,
isActive: true,
createdAt: now,
};

Expand All @@ -673,12 +684,36 @@ export async function deleteApiKey(id) {
return true;
}

/**
* Get API key by ID
*/
export async function getApiKeyById(id) {
const db = await getDb();
return db.data.apiKeys.find(k => k.id === id) || null;
}

/**
* Update API key
*/
export async function updateApiKey(id, data) {
const db = await getDb();
const index = db.data.apiKeys.findIndex(k => k.id === id);
if (index === -1) return null;
db.data.apiKeys[index] = {
...db.data.apiKeys[index],
...data,
};
await db.write();
return db.data.apiKeys[index];
}

/**
* Validate API key
*/
export async function validateApiKey(key) {
const db = await getDb();
return db.data.apiKeys.some(k => k.key === key);
const found = db.data.apiKeys.find(k => k.key === key);
return found && found.isActive !== false;
}

// ============ Data Cleanup ============
Expand Down