Skip to content

Commit b30b0ee

Browse files
authored
fix(api-keys): mint real control-plane keys, not local ones (#14)
* fix(api-keys): mint real control-plane keys, not local ones Dashboard API-key creation wrote to a local table, so the keys never authenticated against the CP/engine (they looked created but didn't work). Proxy to the CP's real endpoints instead: POST/GET/DELETE /admin/v1/orgs/{orgId}/api-keys via the session. Now a created key is a genuine CP key that works against the API. * fix(api-keys): map CP key dates to Date for the page type
1 parent 4480b69 commit b30b0ee

1 file changed

Lines changed: 58 additions & 103 deletions

File tree

apps/web/lib/actions/api-keys.ts

Lines changed: 58 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,88 @@
11
"use server";
22

3-
import { getServerSession } from "@/lib/server-auth";
4-
import { db } from "@/db";
5-
import { apiKeys } from "@/db/schema";
6-
import { eq, and, isNull } from "drizzle-orm";
3+
import { forwardToCP, getPrimaryOrgId } from "@/lib/cp-proxy";
74

8-
function generateId(): string {
9-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
10-
let result = "";
11-
const array = new Uint8Array(24);
12-
crypto.getRandomValues(array);
13-
for (const byte of array) {
14-
result += chars[byte % chars.length];
15-
}
16-
return result;
17-
}
18-
19-
function generateApiKey(): string {
20-
const chars =
21-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
22-
let key = "vl_live_sk_";
23-
const array = new Uint8Array(32);
24-
crypto.getRandomValues(array);
25-
for (const byte of array) {
26-
key += chars[byte % chars.length];
27-
}
28-
return key;
29-
}
5+
// API keys are minted by the CONTROL PLANE (POST /admin/v1/orgs/{orgId}/
6+
// api-keys), which is what actually authenticates requests to the engine.
7+
// The dashboard used to write keys to a *local* table — those never worked
8+
// against the API. These actions now proxy to the CP so keys are real.
309

31-
async function hashKey(key: string): Promise<string> {
32-
const encoder = new TextEncoder();
33-
const data = encoder.encode(key);
34-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
35-
const hashArray = Array.from(new Uint8Array(hashBuffer));
36-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
10+
interface CpAPIKey {
11+
id: string;
12+
name: string;
13+
prefix: string;
14+
scopes?: string[];
15+
last_used_at?: string | null;
16+
created_at?: string;
3717
}
3818

3919
export async function createApiKey(data: {
4020
name: string;
4121
permissions?: string;
4222
expiresAt?: string;
4323
}) {
44-
const session = await getServerSession();
45-
if (!session) {
46-
return { error: "Unauthorized" };
47-
}
48-
49-
try {
50-
const rawKey = generateApiKey();
51-
const keyHash = await hashKey(rawKey);
52-
const keyPrefix = rawKey.slice(0, 12);
53-
const id = generateId();
24+
const orgId = await getPrimaryOrgId();
25+
if (!orgId) return { error: "No organization found on your account." };
5426

55-
await db.insert(apiKeys).values({
56-
id,
57-
userId: session.user.id,
58-
name: data.name,
59-
keyPrefix,
60-
keyHash,
61-
permissions: data.permissions || "full",
62-
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
63-
});
64-
65-
// Return the raw key - it can only be shown once
66-
return {
67-
success: true,
68-
key: rawKey,
69-
keyId: id,
70-
name: data.name,
71-
prefix: keyPrefix,
72-
};
73-
} catch (err) {
27+
const { ok, status, data: resp } = await forwardToCP(
28+
`/admin/v1/orgs/${orgId}/api-keys`,
29+
{ method: "POST", orgId, body: { name: data.name } },
30+
);
31+
if (!ok) {
7432
return {
7533
error:
76-
err instanceof Error ? err.message : "Failed to create API key",
34+
(resp as { error?: string } | null)?.error ??
35+
`Failed to create API key (HTTP ${status})`,
7736
};
7837
}
38+
39+
const r = (resp ?? {}) as CpAPIKey & { key?: string };
40+
return {
41+
success: true,
42+
key: r.key, // plaintext, shown once
43+
keyId: r.id,
44+
name: r.name ?? data.name,
45+
prefix: r.prefix ?? r.key?.slice(0, 12),
46+
};
7947
}
8048

8149
export async function revokeApiKey(keyId: string) {
82-
const session = await getServerSession();
83-
if (!session) {
84-
return { error: "Unauthorized" };
85-
}
50+
const orgId = await getPrimaryOrgId();
51+
if (!orgId) return { error: "No organization found on your account." };
8652

87-
try {
88-
await db
89-
.update(apiKeys)
90-
.set({ revokedAt: new Date() })
91-
.where(and(eq(apiKeys.id, keyId), eq(apiKeys.userId, session.user.id)));
92-
93-
return { success: true };
94-
} catch (err) {
53+
const { ok, status, data } = await forwardToCP(
54+
`/admin/v1/orgs/${orgId}/api-keys/${keyId}`,
55+
{ method: "DELETE", orgId },
56+
);
57+
if (!ok) {
9558
return {
9659
error:
97-
err instanceof Error ? err.message : "Failed to revoke API key",
60+
(data as { error?: string } | null)?.error ??
61+
`Failed to revoke API key (HTTP ${status})`,
9862
};
9963
}
64+
return { success: true };
10065
}
10166

10267
export async function listApiKeys() {
103-
const session = await getServerSession();
104-
if (!session) {
105-
return { error: "Unauthorized", keys: [] };
106-
}
68+
const orgId = await getPrimaryOrgId();
69+
if (!orgId) return { error: "No organization found", keys: [] };
10770

108-
try {
109-
const keys = await db
110-
.select({
111-
id: apiKeys.id,
112-
name: apiKeys.name,
113-
keyPrefix: apiKeys.keyPrefix,
114-
permissions: apiKeys.permissions,
115-
rateLimit: apiKeys.rateLimit,
116-
lastUsedAt: apiKeys.lastUsedAt,
117-
expiresAt: apiKeys.expiresAt,
118-
createdAt: apiKeys.createdAt,
119-
})
120-
.from(apiKeys)
121-
.where(
122-
and(eq(apiKeys.userId, session.user.id), isNull(apiKeys.revokedAt))
123-
);
71+
const { ok, data } = await forwardToCP(`/admin/v1/orgs/${orgId}/api-keys`, {
72+
orgId,
73+
});
74+
if (!ok) return { error: "Failed to list API keys", keys: [] };
12475

125-
return { success: true, keys };
126-
} catch (err) {
127-
return {
128-
error:
129-
err instanceof Error ? err.message : "Failed to list API keys",
130-
keys: [],
131-
};
132-
}
76+
const keys = ((data as CpAPIKey[]) ?? []).map((k) => ({
77+
id: k.id,
78+
name: k.name,
79+
keyPrefix: k.prefix,
80+
permissions: (k.scopes ?? []).join(", ") || "full",
81+
rateLimit: null as number | null,
82+
lastUsedAt: k.last_used_at ? new Date(k.last_used_at) : null,
83+
expiresAt: null as Date | null,
84+
createdAt: k.created_at ? new Date(k.created_at) : new Date(),
85+
}));
86+
87+
return { success: true, keys };
13388
}

0 commit comments

Comments
 (0)