added api key migration
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
server/src/internal/dev/api-keys/apiKeyUtils.ts
Normal file
118
server/src/internal/dev/api-keys/apiKeyUtils.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,4 +8,6 @@ export type ApiKey = {
|
||||
prefix: string;
|
||||
created_at: number;
|
||||
env: AppEnv;
|
||||
hashed_key: string;
|
||||
meta: any;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user