added api key migration

This commit is contained in:
John Yeo
2025-02-09 13:17:46 +00:00
parent fc0fa015c5
commit 8dd8dc46ff
7 changed files with 213 additions and 66 deletions

View File

@@ -18,7 +18,7 @@ import { useDevContext } from "./DevContext";
const CreateAPIKey = () => {
const { env, mutate } = useDevContext();
const axiosInstance = useAxiosInstance({env});
const axiosInstance = useAxiosInstance({ env });
const [loading, setLoading] = useState(false);
const [name, setName] = useState("");
@@ -62,7 +62,7 @@ const CreateAPIKey = () => {
Create API Key
</Button>
</DialogTrigger>
<DialogContent className="max-w-[450px]">
<DialogContent className="max-w-[520px]">
<DialogHeader>
<DialogTitle>Create API Key</DialogTitle>
</DialogHeader>

View File

@@ -15,8 +15,14 @@ const apiRouter = Router();
apiRouter.use(apiAuthMiddleware);
apiRouter.use(pricingMiddleware);
apiRouter.use(attachRouter);
apiRouter.get("/auth", (req: any, res) => {
res.json({
message: `Authenticated -- Hello ${req.minOrg?.slug}!`,
});
});
apiRouter.use("/customers", cusRouter);
apiRouter.use("/products", productApiRouter);
apiRouter.use("/features", featureApiRouter);

View File

@@ -32,4 +32,37 @@ export class ApiKeyService {
return count;
}
static async getByHashedKey(sb: SupabaseClient, hashedKey: string) {
const { data, error } = await sb
.from("api_keys")
.select("*")
.eq("hashed_key", hashedKey)
.single();
if (error) {
if (error.code === "PGRST116") {
return null;
}
throw new Error("Failed to get API key");
}
return data;
}
static async update({
sb,
update,
keyId,
}: {
sb: SupabaseClient;
update: any;
keyId: string;
}) {
const { error } = await sb.from("api_keys").update(update).eq("id", keyId);
if (error) {
throw new Error("Failed to update API key");
}
}
}

View File

@@ -0,0 +1,118 @@
import { generateId } from "@/utils/genUtils.js";
import { ApiKey, AppEnv } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import crypto from "crypto";
import { ApiKeyService } from "../ApiKeyService.js";
function generateApiKey(length = 32, prefix = "") {
try {
// Define allowed characters (alphanumeric only)
const allowedChars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const array = new Uint8Array(length);
crypto.getRandomValues(array);
// Convert random bytes to alphanumeric string
const key = Array.from(array)
.map((byte) => allowedChars[byte % allowedChars.length])
.join("");
return prefix ? `${prefix}_${key}` : key;
} catch (error) {
console.error("Failed to generate API key:", error);
throw new Error("Failed to generate secure API key");
}
}
const hashApiKey = (apiKey: string) => {
return crypto.createHash("sha256").update(apiKey).digest("hex");
};
export const createKey = async ({
sb,
env,
name,
orgId,
prefix,
meta,
}: {
sb: SupabaseClient;
env: AppEnv;
name: string;
orgId: string;
prefix: string;
meta: any;
}) => {
const apiKey = generateApiKey(42, prefix);
const hashedKey = hashApiKey(apiKey);
const apiKeyData: ApiKey = {
id: generateId("key"),
org_id: orgId,
user_id: "",
name,
prefix: apiKey.substring(0, 14),
created_at: Date.now(),
env,
hashed_key: hashedKey,
meta,
};
await ApiKeyService.insert(sb, apiKeyData);
return apiKey;
};
export const verifyKey = async ({
sb,
key,
}: {
sb: SupabaseClient;
key: string;
}) => {
const hashedKey = hashApiKey(key);
const apiKey = await ApiKeyService.getByHashedKey(sb, hashedKey);
if (!apiKey) {
return {
valid: false,
data: null,
};
}
return {
valid: true,
data: apiKey,
};
};
export const migrateKey = async ({
sb,
keyId,
meta,
apiKey,
}: {
sb: SupabaseClient;
keyId: string;
meta: any;
apiKey: string;
}) => {
try {
const hashedKey = hashApiKey(apiKey);
await ApiKeyService.update({
sb,
update: {
id: keyId,
hashed_key: hashedKey,
meta,
},
keyId,
});
console.log(`MIGRATED KEY FOR ${keyId}, ${meta.org_slug}`);
} catch (error) {
console.log(`ERROR: FAILED TO MIGRATE KEY FOR ${keyId}, ${meta.org_slug}`);
console.log(error);
}
};

View File

@@ -1,9 +1,10 @@
import { createKey, deleteKey, updateKey } from "@/external/unkeyUtils.js";
import { deleteKey } from "@/external/unkeyUtils.js";
import { withOrgAuth } from "@/middleware/authMiddleware.js";
import { ApiKey, AppEnv } from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import { Router } from "express";
import { ApiKeyService } from "./ApiKeyService.js";
import { OrgService } from "../orgs/OrgService.js";
import { createKey } from "./api-keys/apiKeyUtils.js";
export const devRouter = Router();
@@ -23,40 +24,23 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) => {
const { name } = req.body;
// 1. Create API key
let prefix = "am_test";
let prefix = "am_sk_test";
if (env === AppEnv.Live) {
prefix = "am_live";
prefix = "am_sk_live";
}
const apiKey = await createKey({
sb: req.sb,
env,
name,
ownerId: orgId,
orgId,
prefix,
meta: {
org_slug: req.minOrg.slug,
},
});
if (!apiKey.result) {
console.error("Failed to create API key", apiKey);
res.status(500).json({ error: "Failed to create API key" });
return;
}
const apiKeyData: ApiKey = {
id: apiKey.result!.keyId,
org_id: orgId,
user_id: req.user.id,
name,
prefix: apiKey.result!.key.substring(0, 10),
created_at: Date.now(),
env,
};
await ApiKeyService.insert(req.sb, apiKeyData);
res.status(200).json({
api_key: apiKey.result!.key,
api_key: apiKey,
});
});
@@ -69,8 +53,6 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => {
res.status(404).json({ error: "API key not found" });
return;
}
await deleteKey(id);
} catch (error) {
console.error("Failed to delete API key", error);
res.status(500).json({ error: "Failed to delete API key" });

View File

@@ -1,27 +1,64 @@
import { validateApiKey } from "@/external/unkeyUtils.js";
import { withOrgAuth } from "./authMiddleware.js";
import { migrateKey, verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
const API_KEY_LENGTH = 32;
export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
const authHeader =
req.headers["authorization"] || req.headers["Authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) {
console.log("Invalid API key / token");
res.status(401).json({ message: "Invalid API key / token" });
console.log("No authorization header");
res
.status(401)
.json({ message: "Unauthorized -- did you forget to add an API key?" });
return;
}
const apiKey = authHeader.split(" ")[1];
if (!apiKey.startsWith("am_") || apiKey.length !== API_KEY_LENGTH) {
// console.log("Invalid API Key, verifying clerk token");
if (!apiKey.startsWith("am_")) {
withOrgAuth(req, res, next);
return;
}
// Try verify via Autumn
try {
const timeStart = Date.now();
const { valid, data } = await verifyKey({ sb: req.sb, key: apiKey });
const timeEnd = Date.now();
console.log(`Time taken to verify key: ${timeEnd - timeStart}ms`);
if (valid && data) {
console.log(
`Autumn API verification successful for ${data.meta.org_slug} (${data.env})`
);
req.orgId = data.org_id;
req.env = data.env;
req.minOrg = {
id: data.org_id,
slug: data.meta.org_slug,
};
next();
return;
} else {
console.log(`Autumn API verification failed`);
}
} catch (error) {
console.log("Failed to fetch key from Autumn");
}
// Fallback: Verify via Unkey
try {
const result = await validateApiKey(apiKey);
await migrateKey({
sb: req.sb,
keyId: result.keyId ?? "",
meta: { org_slug: result.meta?.org_slug },
apiKey,
});
console.log(`Unkey verification successul for ${result.meta?.org_slug}`);
req.orgId = result.ownerId;
req.env = result.environment;
req.minOrg = {
@@ -31,39 +68,8 @@ export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
next();
} catch (error) {
console.log("Failed to verify API Key");
console.log("Unkey API verification failed");
withOrgAuth(req, res, next);
return;
}
};
export const wsAuthMiddleware = async (req: any) => {
const authHeader = req.headers["authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return { data: null, error: "Unauthorized" };
}
const apiKey = authHeader.split(" ")[1];
if (!apiKey.startsWith("am_") || apiKey.length !== API_KEY_LENGTH) {
return { data: null, error: "Unauthorized" };
}
try {
const result = await validateApiKey(apiKey);
return {
data: {
env: result.environment,
orgId: result.ownerId,
minOrg: {
id: result.ownerId,
slug: result.meta?.org_slug,
},
},
error: null,
};
} catch (error) {
return { data: null, error: "Unauthorized" };
}
};

View File

@@ -8,4 +8,6 @@ export type ApiKey = {
prefix: string;
created_at: number;
env: AppEnv;
hashed_key: string;
meta: any;
};