143 lines
3.5 KiB
TypeScript
143 lines
3.5 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query } from "./_generated/server";
|
|
|
|
/**
|
|
* API Key Management for Secure Workflow API Access
|
|
*/
|
|
|
|
// Generate secure random token
|
|
function generateSecureToken(length: number): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Simple hash function
|
|
function hashKey(key: string): string {
|
|
// Simple hash for Convex environment
|
|
// In production, consider using a more robust hashing method
|
|
let hash = 0;
|
|
for (let i = 0; i < key.length; i++) {
|
|
const char = key.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash; // Convert to 32bit integer
|
|
}
|
|
return 'hash_' + Math.abs(hash).toString(36) + '_' + key.length;
|
|
}
|
|
|
|
// Generate new API key
|
|
export const generate = mutation({
|
|
args: {
|
|
name: v.string(),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
// Generate secure key
|
|
const key = `sk_live_${generateSecureToken(32)}`;
|
|
const keyHash = hashKey(key);
|
|
const keyPrefix = key.substring(0, 15) + "...";
|
|
|
|
const apiKeyId = await ctx.db.insert("apiKeys", {
|
|
key: keyHash,
|
|
keyPrefix,
|
|
userId: identity.subject,
|
|
name: args.name,
|
|
usageCount: 0,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
// Return plain key ONCE (never shown again!)
|
|
return {
|
|
id: apiKeyId,
|
|
key, // Only time user sees this!
|
|
keyPrefix,
|
|
name: args.name,
|
|
};
|
|
},
|
|
});
|
|
|
|
// List user's API keys (without actual key values)
|
|
export const list = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) return [];
|
|
|
|
return await ctx.db
|
|
.query("apiKeys")
|
|
.withIndex("by_userId", (q) => q.eq("userId", identity.subject))
|
|
.filter((q) => q.eq(q.field("revokedAt"), undefined))
|
|
.order("desc")
|
|
.collect();
|
|
},
|
|
});
|
|
|
|
// Revoke API key
|
|
export const revoke = mutation({
|
|
args: { id: v.id("apiKeys") },
|
|
handler: async (ctx, args) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
const apiKey = await ctx.db.get(args.id);
|
|
if (!apiKey) {
|
|
throw new Error("API key not found");
|
|
}
|
|
|
|
if (apiKey.userId !== identity.subject) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
await ctx.db.patch(args.id, {
|
|
revokedAt: new Date().toISOString(),
|
|
});
|
|
|
|
return { success: true };
|
|
},
|
|
});
|
|
|
|
// Verify API key (called by middleware)
|
|
export const verify = mutation({
|
|
args: { key: v.string() },
|
|
handler: async (ctx, args) => {
|
|
const keyHash = hashKey(args.key);
|
|
|
|
const apiKey = await ctx.db
|
|
.query("apiKeys")
|
|
.withIndex("by_key", (q) => q.eq("key", keyHash))
|
|
.first();
|
|
|
|
if (!apiKey) {
|
|
return { valid: false, error: "Invalid API key" };
|
|
}
|
|
|
|
if (apiKey.revokedAt) {
|
|
return { valid: false, error: "API key revoked" };
|
|
}
|
|
|
|
if (apiKey.expiresAt && new Date(apiKey.expiresAt) < new Date()) {
|
|
return { valid: false, error: "API key expired" };
|
|
}
|
|
|
|
// Update usage stats
|
|
await ctx.db.patch(apiKey._id, {
|
|
lastUsedAt: new Date().toISOString(),
|
|
usageCount: apiKey.usageCount + 1,
|
|
});
|
|
|
|
return {
|
|
valid: true,
|
|
userId: apiKey.userId,
|
|
};
|
|
},
|
|
});
|