added api key migration
This commit is contained in:
@@ -18,7 +18,7 @@ import { useDevContext } from "./DevContext";
|
|||||||
|
|
||||||
const CreateAPIKey = () => {
|
const CreateAPIKey = () => {
|
||||||
const { env, mutate } = useDevContext();
|
const { env, mutate } = useDevContext();
|
||||||
const axiosInstance = useAxiosInstance({env});
|
const axiosInstance = useAxiosInstance({ env });
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -62,7 +62,7 @@ const CreateAPIKey = () => {
|
|||||||
Create API Key
|
Create API Key
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-[450px]">
|
<DialogContent className="max-w-[520px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create API Key</DialogTitle>
|
<DialogTitle>Create API Key</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -15,8 +15,14 @@ const apiRouter = Router();
|
|||||||
|
|
||||||
apiRouter.use(apiAuthMiddleware);
|
apiRouter.use(apiAuthMiddleware);
|
||||||
apiRouter.use(pricingMiddleware);
|
apiRouter.use(pricingMiddleware);
|
||||||
|
|
||||||
apiRouter.use(attachRouter);
|
apiRouter.use(attachRouter);
|
||||||
|
|
||||||
|
apiRouter.get("/auth", (req: any, res) => {
|
||||||
|
res.json({
|
||||||
|
message: `Authenticated -- Hello ${req.minOrg?.slug}!`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
apiRouter.use("/customers", cusRouter);
|
apiRouter.use("/customers", cusRouter);
|
||||||
apiRouter.use("/products", productApiRouter);
|
apiRouter.use("/products", productApiRouter);
|
||||||
apiRouter.use("/features", featureApiRouter);
|
apiRouter.use("/features", featureApiRouter);
|
||||||
|
|||||||
@@ -32,4 +32,37 @@ export class ApiKeyService {
|
|||||||
|
|
||||||
return count;
|
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 { withOrgAuth } from "@/middleware/authMiddleware.js";
|
||||||
import { ApiKey, AppEnv } from "@autumn/shared";
|
import { AppEnv } from "@autumn/shared";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ApiKeyService } from "./ApiKeyService.js";
|
import { ApiKeyService } from "./ApiKeyService.js";
|
||||||
import { OrgService } from "../orgs/OrgService.js";
|
import { OrgService } from "../orgs/OrgService.js";
|
||||||
|
import { createKey } from "./api-keys/apiKeyUtils.js";
|
||||||
|
|
||||||
export const devRouter = Router();
|
export const devRouter = Router();
|
||||||
|
|
||||||
@@ -23,40 +24,23 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) => {
|
|||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
// 1. Create API key
|
// 1. Create API key
|
||||||
let prefix = "am_test";
|
let prefix = "am_sk_test";
|
||||||
if (env === AppEnv.Live) {
|
if (env === AppEnv.Live) {
|
||||||
prefix = "am_live";
|
prefix = "am_sk_live";
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = await createKey({
|
const apiKey = await createKey({
|
||||||
|
sb: req.sb,
|
||||||
env,
|
env,
|
||||||
name,
|
name,
|
||||||
ownerId: orgId,
|
orgId,
|
||||||
prefix,
|
prefix,
|
||||||
meta: {
|
meta: {
|
||||||
org_slug: req.minOrg.slug,
|
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({
|
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" });
|
res.status(404).json({ error: "API key not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteKey(id);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete API key", error);
|
console.error("Failed to delete API key", error);
|
||||||
res.status(500).json({ error: "Failed to delete API key" });
|
res.status(500).json({ error: "Failed to delete API key" });
|
||||||
|
|||||||
@@ -1,27 +1,64 @@
|
|||||||
import { validateApiKey } from "@/external/unkeyUtils.js";
|
import { validateApiKey } from "@/external/unkeyUtils.js";
|
||||||
import { withOrgAuth } from "./authMiddleware.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) => {
|
export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
|
||||||
const authHeader =
|
const authHeader =
|
||||||
req.headers["authorization"] || req.headers["Authorization"];
|
req.headers["authorization"] || req.headers["Authorization"];
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
console.log("Invalid API key / token");
|
console.log("No authorization header");
|
||||||
res.status(401).json({ message: "Invalid API key / token" });
|
res
|
||||||
|
.status(401)
|
||||||
|
.json({ message: "Unauthorized -- did you forget to add an API key?" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = authHeader.split(" ")[1];
|
const apiKey = authHeader.split(" ")[1];
|
||||||
if (!apiKey.startsWith("am_") || apiKey.length !== API_KEY_LENGTH) {
|
if (!apiKey.startsWith("am_")) {
|
||||||
// console.log("Invalid API Key, verifying clerk token");
|
|
||||||
withOrgAuth(req, res, next);
|
withOrgAuth(req, res, next);
|
||||||
return;
|
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 {
|
try {
|
||||||
const result = await validateApiKey(apiKey);
|
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.orgId = result.ownerId;
|
||||||
req.env = result.environment;
|
req.env = result.environment;
|
||||||
req.minOrg = {
|
req.minOrg = {
|
||||||
@@ -31,39 +68,8 @@ export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to verify API Key");
|
console.log("Unkey API verification failed");
|
||||||
withOrgAuth(req, res, next);
|
withOrgAuth(req, res, next);
|
||||||
return;
|
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;
|
prefix: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
|
hashed_key: string;
|
||||||
|
meta: any;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user