mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-30 12:42:22 +00:00
Add ability to predefine a root api key
This commit is contained in:
106
server/setup/ensureRootApiKey.ts
Normal file
106
server/setup/ensureRootApiKey.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { db, apiKeys } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
||||||
|
import moment from "moment";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
|
||||||
|
const random: RandomReader = {
|
||||||
|
read(bytes: Uint8Array): void {
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateApiKeyId(id: string): boolean {
|
||||||
|
return /^[a-z0-9]{15}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateApiKeySecret(secret: string): boolean {
|
||||||
|
return secret.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRootApiKey(apiKeyId: string, source: string): void {
|
||||||
|
console.log(`=== ROOT API KEY ${source} ===`);
|
||||||
|
console.log("API Key ID:", apiKeyId);
|
||||||
|
console.log(
|
||||||
|
"The root API key from PANGOLIN_ROOT_API_KEY has been applied."
|
||||||
|
);
|
||||||
|
console.log("Use the full key value (apiKeyId.apiKeySecret) in requests.");
|
||||||
|
console.log("================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureRootApiKey() {
|
||||||
|
try {
|
||||||
|
const envApiKey = process.env.PANGOLIN_ROOT_API_KEY;
|
||||||
|
|
||||||
|
if (!envApiKey) {
|
||||||
|
logger.debug(
|
||||||
|
"PANGOLIN_ROOT_API_KEY not set. Root API key from environment skipped."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = envApiKey.split(".");
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid format for PANGOLIN_ROOT_API_KEY. Expected format: {apiKeyId}.{apiKeySecret}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [apiKeyId, apiKeySecret] = parts;
|
||||||
|
|
||||||
|
if (!validateApiKeyId(apiKeyId)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid apiKeyId in PANGOLIN_ROOT_API_KEY. Must be 15 lowercase alphanumeric characters."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateApiKeySecret(apiKeySecret)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid apiKeySecret in PANGOLIN_ROOT_API_KEY. Secret must not be empty."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyHash = await hashPassword(apiKeySecret);
|
||||||
|
const lastChars = apiKeySecret.slice(-4);
|
||||||
|
const createdAt = moment().toISOString();
|
||||||
|
|
||||||
|
const [existingKey] = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeys)
|
||||||
|
.where(eq(apiKeys.apiKeyId, apiKeyId));
|
||||||
|
|
||||||
|
if (existingKey) {
|
||||||
|
if (!existingKey.isRoot) {
|
||||||
|
console.warn(
|
||||||
|
`API key with ID ${apiKeyId} exists but is not a root key. Promoting to root and updating hash.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Overwriting existing root API key hash since PANGOLIN_ROOT_API_KEY is set (apiKeyId: ${apiKeyId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(apiKeys)
|
||||||
|
.set({ apiKeyHash, lastChars, isRoot: true })
|
||||||
|
.where(eq(apiKeys.apiKeyId, apiKeyId));
|
||||||
|
|
||||||
|
showRootApiKey(apiKeyId, "UPDATED FROM ENVIRONMENT");
|
||||||
|
} else {
|
||||||
|
await db.insert(apiKeys).values({
|
||||||
|
apiKeyId,
|
||||||
|
name: "Root API Key (Environment)",
|
||||||
|
apiKeyHash,
|
||||||
|
lastChars,
|
||||||
|
createdAt,
|
||||||
|
isRoot: true
|
||||||
|
});
|
||||||
|
|
||||||
|
showRootApiKey(apiKeyId, "CREATED FROM ENVIRONMENT");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to ensure root API key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import { ensureActions } from "./ensureActions";
|
|||||||
import { copyInConfig } from "./copyInConfig";
|
import { copyInConfig } from "./copyInConfig";
|
||||||
import { clearStaleData } from "./clearStaleData";
|
import { clearStaleData } from "./clearStaleData";
|
||||||
import { ensureSetupToken } from "./ensureSetupToken";
|
import { ensureSetupToken } from "./ensureSetupToken";
|
||||||
|
import { ensureRootApiKey } from "./ensureRootApiKey";
|
||||||
|
|
||||||
export async function runSetupFunctions() {
|
export async function runSetupFunctions() {
|
||||||
await copyInConfig(); // copy in the config to the db as needed
|
await copyInConfig(); // copy in the config to the db as needed
|
||||||
await ensureActions(); // make sure all of the actions are in the db and the roles
|
await ensureActions(); // make sure all of the actions are in the db and the roles
|
||||||
await clearStaleData();
|
await clearStaleData();
|
||||||
await ensureSetupToken(); // ensure setup token exists for initial setup
|
await ensureSetupToken(); // ensure setup token exists for initial setup
|
||||||
|
await ensureRootApiKey();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user