|
1 | 1 | "use server"; |
2 | 2 |
|
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"; |
7 | 4 |
|
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. |
30 | 9 |
|
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; |
37 | 17 | } |
38 | 18 |
|
39 | 19 | export async function createApiKey(data: { |
40 | 20 | name: string; |
41 | 21 | permissions?: string; |
42 | 22 | expiresAt?: string; |
43 | 23 | }) { |
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." }; |
54 | 26 |
|
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) { |
74 | 32 | return { |
75 | 33 | 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})`, |
77 | 36 | }; |
78 | 37 | } |
| 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 | + }; |
79 | 47 | } |
80 | 48 |
|
81 | 49 | 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." }; |
86 | 52 |
|
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) { |
95 | 58 | return { |
96 | 59 | 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})`, |
98 | 62 | }; |
99 | 63 | } |
| 64 | + return { success: true }; |
100 | 65 | } |
101 | 66 |
|
102 | 67 | 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: [] }; |
107 | 70 |
|
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: [] }; |
124 | 75 |
|
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 }; |
133 | 88 | } |
0 commit comments