Compare commits

...

8 Commits

11 changed files with 200 additions and 32 deletions
+2 -1
View File
@@ -25,7 +25,8 @@
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
"utils:create_admin_role": "bun ./utils/createAdminRole",
"utils:assign_user_role": "bun ./utils/assignUserRole"
"utils:assign_user_role": "bun ./utils/assignUserRole",
"db:check": "bunx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --shadow-database-url $DATABASE_URL --exit-code"
},
"dependencies": {
"@azure/msal-node": "^5.0.2",
@@ -0,0 +1,63 @@
-- CreateTable
CREATE TABLE "UnifiSite" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"siteId" TEXT NOT NULL,
"companyId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UnifiSite_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CatalogItem" (
"id" TEXT NOT NULL,
"cwCatalogId" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"customerDescription" TEXT,
"internalNotes" TEXT,
"manufacturer" TEXT,
"manufactureCwId" INTEGER,
"partNumber" TEXT,
"vendorName" TEXT,
"vendorSku" TEXT,
"vendorCwId" INTEGER,
"price" DOUBLE PRECISION NOT NULL,
"cost" DOUBLE PRECISION NOT NULL,
"inactive" BOOLEAN NOT NULL DEFAULT false,
"salesTaxable" BOOLEAN NOT NULL DEFAULT true,
"onHand" INTEGER NOT NULL DEFAULT 0,
"cwLastUpdated" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CatalogItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_LinkedItems" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_LinkedItems_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "UnifiSite_siteId_key" ON "UnifiSite"("siteId");
-- CreateIndex
CREATE UNIQUE INDEX "CatalogItem_cwCatalogId_key" ON "CatalogItem"("cwCatalogId");
-- CreateIndex
CREATE INDEX "_LinkedItems_B_index" ON "_LinkedItems"("B");
-- AddForeignKey
ALTER TABLE "UnifiSite" ADD CONSTRAINT "UnifiSite_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_A_fkey" FOREIGN KEY ("A") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_B_fkey" FOREIGN KEY ("B") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+2 -2
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import * as msal from "@azure/msal-node";
import { io, msalClient } from "../../constants";
import { API_BASE_URL, io, msalClient } from "../../constants";
import { users } from "../../managers/users";
/* /v1/auth/redirect */
@@ -11,7 +11,7 @@ export default createRoute("get", ["/redirect"], async (c) => {
const tokenRequest: msal.AuthorizationCodeRequest = {
code: c.req.query().code as string,
scopes: ["user.read"],
redirectUri: "http://localhost:3000/v1/auth/redirect",
redirectUri: `${API_BASE_URL}/v1/auth/redirect`,
};
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
+3 -1
View File
@@ -1,5 +1,6 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import { API_BASE_URL } from "../../constants";
import cuid from "cuid";
/* /v1/auth/uri */
@@ -7,7 +8,8 @@ export default createRoute("get", ["/uri"], (c) => {
c.status(200);
const callbackKey = cuid();
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fv1%2Fauth%2Fredirect&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
const redirectUri = encodeURIComponent(`${API_BASE_URL}/v1/auth/redirect`);
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=${redirectUri}&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
return c.json({
status: 200,
+2
View File
@@ -17,6 +17,8 @@ interface EnvKey {
// ENV CONSTANTS
export const PORT = process.env.PORT;
export const API_BASE_URL =
process.env.API_BASE_URL || `http://localhost:${PORT || 3000}`;
export const prisma = new PrismaClient({ adapter });
+35 -1
View File
@@ -1,15 +1,49 @@
import { refresh } from "./api/auth";
import app from "./api/server";
import { engine, PORT, unifi, unifiPassword, unifiUsername } from "./constants";
import {
engine,
PORT,
prisma,
unifi,
unifiPassword,
unifiUsername,
} from "./constants";
import { unifiSites } from "./managers/unifiSites";
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
import { events, setupEventDebugger } from "./modules/globalEvents";
import { signPermissions } from "./modules/permission-utils/signPermissions";
import { RoleController } from "./controllers/RoleController";
import cuid from "cuid";
// Setup global event debugger in non-production environments
if (Bun.env.NODE_ENV == "development") setupEventDebugger();
// Ensure administrator role exists
const existingAdmin = await prisma.role.findFirst({
where: { moniker: "administrator" },
include: { users: { include: { roles: true } } },
});
if (!existingAdmin) {
const id = cuid();
const created = await prisma.role.create({
data: {
id,
moniker: "administrator",
title: "Admin",
permissions: signPermissions({
issuer: "roles",
subject: id,
permissions: ["*"],
}),
},
include: { users: { include: { roles: true } } },
});
events.emit("role:created", new RoleController(created));
}
// Refresh the internal list of companies every minute
await refreshCompanies();
setInterval(() => {
@@ -6,12 +6,8 @@ export const generateSecureValue = (content: string) => {
// Generate a hash of the content
const hash = Password.hash(content);
// Parse the PKCS#1 PEM key into a proper KeyObject
const publicKey = crypto.createPublicKey({
key: secureValuesPublicKey,
format: "pem",
type: "pkcs1",
});
// Parse the PEM key into a proper KeyObject
const publicKey = crypto.createPublicKey(secureValuesPublicKey);
// Encrypt the content using the .secureValues.pub public key
const encrypted = crypto.publicEncrypt(
+1 -5
View File
@@ -3,11 +3,7 @@ import crypto from "crypto";
import { secureValuesPrivateKey } from "../../constants";
import GenericError from "../../Errors/GenericError";
const privateKey = crypto.createPrivateKey({
key: secureValuesPrivateKey,
format: "pem",
type: "pkcs1",
});
const privateKey = crypto.createPrivateKey(secureValuesPrivateKey);
export const readSecureValue = (
encryptedContent: string,
+65
View File
@@ -0,0 +1,65 @@
import { execSync } from "child_process";
const kubeconfig = "/Users/jroberts/projects/K8S-QuickDeploy/k8s.yaml";
function getKey(name: string): string {
const b64 = execSync(
`KUBECONFIG=${kubeconfig} kubectl get secret optima-keys-secret -n optima -o jsonpath="{.data.${name}}"`,
)
.toString()
.trim();
return Buffer.from(b64, "base64").toString("utf-8");
}
const privKeys = [
"ACCESS_TOKEN_PRIVATE_KEY",
"REFRESH_TOKEN_PRIVATE_KEY",
"PERMISSIONS_PRIVATE_KEY",
"SECURE_VALUES_PRIVATE_KEY",
];
const converted: Record<string, string> = {};
// Use openssl CLI to convert PKCS#1 to PKCS#8 (Bun's crypto has issues with some keys)
for (const k of privKeys) {
const pem = getKey(k);
const pkcs8 = execSync("openssl pkey -in /dev/stdin", {
input: pem,
}).toString();
converted[k] = pkcs8;
console.log(`${k}: converted to PKCS#8 ✅`);
}
const pubPem = getKey("SECURE_VALUES_PUBLIC_KEY");
const spki = execSync("openssl rsa -RSAPublicKey_in -pubout -in /dev/stdin", {
input: pubPem,
}).toString();
converted["SECURE_VALUES_PUBLIC_KEY"] = spki;
console.log("SECURE_VALUES_PUBLIC_KEY: converted to SPKI ✅");
// Generate kubectl command to recreate the secret with PKCS#8 keys
const args = Object.entries(converted)
.map(([k, v]) => `--from-literal=${k}='${v}'`)
.join(" \\\n ");
console.log("\n--- Delete and recreate secret with PKCS#8 keys ---\n");
console.log(
`KUBECONFIG=${kubeconfig} kubectl delete secret optima-keys-secret -n optima`,
);
console.log(
`KUBECONFIG=${kubeconfig} kubectl create secret generic optima-keys-secret -n optima \\\n ${args}`,
);
// Actually do it
console.log("\nApplying...");
execSync(
`KUBECONFIG=${kubeconfig} kubectl delete secret optima-keys-secret -n optima`,
);
const literals = Object.entries(converted).map(
([k, v]) => `--from-literal=${k}=${v}`,
);
const cmd = `KUBECONFIG=${kubeconfig} kubectl create secret generic optima-keys-secret -n optima ${literals.join(" ")}`;
execSync(cmd);
console.log("Secret recreated with PKCS#8 keys ✅");
+9 -5
View File
@@ -1,9 +1,9 @@
import keypair from "keypair";
import crypto from "crypto";
console.log(`
Generating Private Keys
-----------------
This script will go through and genrate all the keys necessary for running the Credential Manager API locally.
This script will go through and generate all the keys necessary for running the Credential Manager API locally.
This process might take several minutes.
-----------------`);
@@ -42,9 +42,13 @@ await Promise.all(
if (!privExists || !pubExists) {
// Always regenerate both files together to ensure the key pair matches
console.log(`Generating '${v}' and '${pubPath}'...`);
const keys = keypair({ bits: 4096 });
await Bun.write(v, keys.private);
await Bun.write(pubPath, keys.public);
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 4096,
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
await Bun.write(v, privateKey);
await Bun.write(pubPath, publicKey);
}
return;
}),
+16 -11
View File
@@ -1,4 +1,4 @@
import keypair from "keypair";
import crypto from "crypto";
import { mkdirSync } from "fs";
const outputDir = "production-keys";
@@ -19,14 +19,18 @@ const generatedKeys: Record<string, { private: string; public: string }> = {};
for (const name of keyFiles) {
console.log(`Generating '${name}' key pair (4096-bit RSA)...`);
const keys = keypair({ bits: 4096 });
generatedKeys[name] = keys;
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 4096,
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
generatedKeys[name] = { private: privateKey, public: publicKey };
const privPath = `${outputDir}/${name}.key`;
const pubPath = `${outputDir}/${name}.pub`;
await Bun.write(privPath, keys.private);
await Bun.write(pubPath, keys.public);
await Bun.write(privPath, privateKey);
await Bun.write(pubPath, publicKey);
console.log(`${privPath}`);
console.log(`${pubPath}`);
@@ -38,14 +42,15 @@ const toBase64 = (str: string) => Buffer.from(str).toString("base64");
const secretYaml = `apiVersion: v1
kind: Secret
metadata:
name: optima-keys
name: optima-keys-secret
namespace: optima
type: Opaque
data:
accessToken.key: ${toBase64(generatedKeys["accessToken"].private)}
refreshToken.key: ${toBase64(generatedKeys["refreshToken"].private)}
permissions.key: ${toBase64(generatedKeys["permissions"].private)}
secureValues.key: ${toBase64(generatedKeys["secureValues"].private)}
secureValues.pub: ${toBase64(generatedKeys["secureValues"].public)}
ACCESS_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["accessToken"].private)}
REFRESH_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["refreshToken"].private)}
PERMISSIONS_PRIVATE_KEY: ${toBase64(generatedKeys["permissions"].private)}
SECURE_VALUES_PRIVATE_KEY: ${toBase64(generatedKeys["secureValues"].private)}
SECURE_VALUES_PUBLIC_KEY: ${toBase64(generatedKeys["secureValues"].public)}
`;
const secretPath = `${outputDir}/optima-keys-secret.yaml`;