Skip to content

Commit ec989ec

Browse files
authored
feat: introduce api key concept. specifically needed for trigger webh… (#395)
1 parent 7782006 commit ec989ec

File tree

4 files changed

+561
-9
lines changed

4 files changed

+561
-9
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getSupabaseClient } from "@/lib/auth/supabase-client";
3+
import { decodeJWT } from "@/lib/jwt-utils";
4+
5+
function isTokenExpired(exp: number): boolean {
6+
const currentTime = Date.now() / 1000;
7+
return currentTime > exp;
8+
}
9+
10+
export async function DELETE(
11+
request: NextRequest,
12+
{ params }: { params: Promise<{ keyId: string }> },
13+
) {
14+
try {
15+
const supabase = getSupabaseClient();
16+
17+
const accessToken = request.headers.get("x-access-token");
18+
const refreshToken = request.headers.get("x-refresh-token");
19+
const jwtSecret = process.env.SUPABASE_JWT_SECRET;
20+
21+
if (!accessToken || !refreshToken || !jwtSecret) {
22+
return NextResponse.json(
23+
{ error: "Authentication required" },
24+
{ status: 401 },
25+
);
26+
}
27+
28+
const payload = decodeJWT(accessToken, jwtSecret);
29+
if (!payload || !payload.sub || isTokenExpired(payload.exp)) {
30+
return NextResponse.json(
31+
{ error: "Invalid or expired token" },
32+
{ status: 401 },
33+
);
34+
}
35+
36+
const userId = payload.sub;
37+
const { keyId } = await params;
38+
39+
if (!keyId) {
40+
return NextResponse.json(
41+
{ error: "API key ID is required" },
42+
{ status: 400 },
43+
);
44+
}
45+
46+
await supabase.auth.setSession({
47+
access_token: accessToken,
48+
refresh_token: refreshToken,
49+
});
50+
51+
// Delete the API key, ensuring it belongs to the authenticated user
52+
const { error } = (await supabase
53+
.from("user_api_keys")
54+
.delete()
55+
.eq("id", keyId)
56+
.eq("user_id", userId)) as any;
57+
58+
if (error) {
59+
console.error("Error deleting user API key:", error);
60+
return NextResponse.json(
61+
{ error: "Failed to delete API key" },
62+
{ status: 500 },
63+
);
64+
}
65+
66+
return NextResponse.json({
67+
message: "API key deleted successfully",
68+
});
69+
} catch (error) {
70+
console.error("User API key deletion error:", error);
71+
return NextResponse.json(
72+
{ error: "Internal server error" },
73+
{ status: 500 },
74+
);
75+
}
76+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getSupabaseClient } from "@/lib/auth/supabase-client";
3+
import { decodeJWT } from "@/lib/jwt-utils";
4+
import { encryptSecret, decryptSecret } from "@/lib/crypto";
5+
6+
function generateApiKey(): string {
7+
const prefix = "oap_";
8+
const chars =
9+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
10+
let result = prefix;
11+
for (let i = 0; i < 32; i++) {
12+
result += chars.charAt(Math.floor(Math.random() * chars.length));
13+
}
14+
return result;
15+
}
16+
17+
function isTokenExpired(exp: number): boolean {
18+
const currentTime = Date.now() / 1000;
19+
return currentTime > exp;
20+
}
21+
22+
function encryptApiKey(apiKey: string): string {
23+
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
24+
if (!encryptionKey) {
25+
throw new Error("Encryption key not found");
26+
}
27+
return encryptSecret(apiKey, encryptionKey);
28+
}
29+
30+
function decryptApiKey(encryptedApiKey: string): string {
31+
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
32+
if (!encryptionKey) {
33+
throw new Error("Encryption key not found");
34+
}
35+
return decryptSecret(encryptedApiKey, encryptionKey);
36+
}
37+
38+
export async function POST(request: NextRequest) {
39+
try {
40+
const supabase = getSupabaseClient();
41+
42+
const accessToken = request.headers.get("x-access-token");
43+
const refreshToken = request.headers.get("x-refresh-token");
44+
const jwtSecret = process.env.SUPABASE_JWT_SECRET;
45+
46+
if (!accessToken || !refreshToken || !jwtSecret) {
47+
return NextResponse.json(
48+
{ error: "Authentication required" },
49+
{ status: 401 },
50+
);
51+
}
52+
53+
const payload = decodeJWT(accessToken, jwtSecret);
54+
if (!payload || !payload.sub || isTokenExpired(payload.exp)) {
55+
return NextResponse.json(
56+
{ error: "Invalid or expired token" },
57+
{ status: 401 },
58+
);
59+
}
60+
61+
const userId = payload.sub;
62+
const body = await request.json();
63+
const { name } = body;
64+
65+
if (!name || typeof name !== "string" || name.trim().length === 0) {
66+
return NextResponse.json(
67+
{ error: "API key name is required" },
68+
{ status: 400 },
69+
);
70+
}
71+
72+
await supabase.auth.setSession({
73+
access_token: accessToken,
74+
refresh_token: refreshToken,
75+
});
76+
77+
// Generate a new API key
78+
const apiKey = generateApiKey();
79+
const encryptedApiKey = encryptApiKey(apiKey);
80+
81+
// Insert into user_api_keys table
82+
const { data, error } = await supabase
83+
.from("user_api_keys")
84+
.insert({
85+
user_id: userId,
86+
name: name.trim(),
87+
key_hash: encryptedApiKey,
88+
created_at: new Date().toISOString(),
89+
} as any)
90+
.select()
91+
.single();
92+
93+
if (error) {
94+
console.error("Error creating user API key:", error);
95+
return NextResponse.json(
96+
{ error: "Failed to create API key" },
97+
{ status: 500 },
98+
);
99+
}
100+
101+
return NextResponse.json({
102+
message: "API key created successfully",
103+
apiKey: {
104+
id: (data as any).id,
105+
name: (data as any).name,
106+
key: apiKey, // Return the plain key only on creation
107+
created_at: (data as any).created_at,
108+
},
109+
});
110+
} catch (error) {
111+
console.error("User API key creation error:", error);
112+
return NextResponse.json(
113+
{ error: "Internal server error" },
114+
{ status: 500 },
115+
);
116+
}
117+
}
118+
119+
export async function GET(request: NextRequest) {
120+
try {
121+
const supabase = getSupabaseClient();
122+
123+
const accessToken = request.headers.get("x-access-token");
124+
const refreshToken = request.headers.get("x-refresh-token");
125+
const jwtSecret = process.env.SUPABASE_JWT_SECRET;
126+
127+
if (!accessToken || !refreshToken || !jwtSecret) {
128+
return NextResponse.json(
129+
{ error: "Authentication required" },
130+
{ status: 401 },
131+
);
132+
}
133+
134+
const payload = decodeJWT(accessToken, jwtSecret);
135+
if (!payload || !payload.sub || isTokenExpired(payload.exp)) {
136+
return NextResponse.json(
137+
{ error: "Invalid or expired token" },
138+
{ status: 401 },
139+
);
140+
}
141+
142+
const userId = payload.sub;
143+
144+
await supabase.auth.setSession({
145+
access_token: accessToken,
146+
refresh_token: refreshToken,
147+
});
148+
149+
const { data, error } = (await supabase
150+
.from("user_api_keys")
151+
.select("id, name, key_hash, created_at")
152+
.eq("user_id", userId)
153+
.order("created_at", { ascending: false })) as any;
154+
155+
if (error) {
156+
console.error("Error fetching user API keys:", error);
157+
return NextResponse.json(
158+
{ error: "Failed to fetch API keys" },
159+
{ status: 500 },
160+
);
161+
}
162+
163+
// Decrypt the API keys for display
164+
const apiKeys = (data as any[]).map((key: any) => ({
165+
id: key.id,
166+
name: key.name,
167+
key: decryptApiKey(key.key_hash),
168+
created_at: key.created_at,
169+
}));
170+
171+
return NextResponse.json({ apiKeys });
172+
} catch (error) {
173+
console.error("User API key fetch error:", error);
174+
return NextResponse.json(
175+
{ error: "Internal server error" },
176+
{ status: 500 },
177+
);
178+
}
179+
}

0 commit comments

Comments
 (0)