Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 827b018f25 | |||
| 5852bd7819 | |||
| 4c21245044 | |||
| ce456257ea | |||
| 8949819396 | |||
| 49faf97c9b | |||
| 05bab2c90f | |||
| 29b5c986cd |
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
|
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
|
||||||
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
|
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
|
||||||
"utils:create_admin_role": "bun ./utils/createAdminRole",
|
"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": {
|
"dependencies": {
|
||||||
"@azure/msal-node": "^5.0.2",
|
"@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;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono/tiny";
|
import { Hono } from "hono/tiny";
|
||||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
import * as msal from "@azure/msal-node";
|
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";
|
import { users } from "../../managers/users";
|
||||||
|
|
||||||
/* /v1/auth/redirect */
|
/* /v1/auth/redirect */
|
||||||
@@ -11,7 +11,7 @@ export default createRoute("get", ["/redirect"], async (c) => {
|
|||||||
const tokenRequest: msal.AuthorizationCodeRequest = {
|
const tokenRequest: msal.AuthorizationCodeRequest = {
|
||||||
code: c.req.query().code as string,
|
code: c.req.query().code as string,
|
||||||
scopes: ["user.read"],
|
scopes: ["user.read"],
|
||||||
redirectUri: "http://localhost:3000/v1/auth/redirect",
|
redirectUri: `${API_BASE_URL}/v1/auth/redirect`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
|
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from "hono/tiny";
|
import { Hono } from "hono/tiny";
|
||||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { API_BASE_URL } from "../../constants";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
|
|
||||||
/* /v1/auth/uri */
|
/* /v1/auth/uri */
|
||||||
@@ -7,7 +8,8 @@ export default createRoute("get", ["/uri"], (c) => {
|
|||||||
c.status(200);
|
c.status(200);
|
||||||
|
|
||||||
const callbackKey = cuid();
|
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({
|
return c.json({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ interface EnvKey {
|
|||||||
// ENV CONSTANTS
|
// ENV CONSTANTS
|
||||||
|
|
||||||
export const PORT = process.env.PORT;
|
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 });
|
export const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
|||||||
+35
-1
@@ -1,15 +1,49 @@
|
|||||||
import { refresh } from "./api/auth";
|
import { refresh } from "./api/auth";
|
||||||
import app from "./api/server";
|
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 { unifiSites } from "./managers/unifiSites";
|
||||||
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
|
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
|
||||||
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
|
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
|
||||||
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
|
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
|
||||||
import { events, setupEventDebugger } from "./modules/globalEvents";
|
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
|
// Setup global event debugger in non-production environments
|
||||||
if (Bun.env.NODE_ENV == "development") setupEventDebugger();
|
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
|
// Refresh the internal list of companies every minute
|
||||||
await refreshCompanies();
|
await refreshCompanies();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
|||||||
@@ -6,12 +6,8 @@ export const generateSecureValue = (content: string) => {
|
|||||||
// Generate a hash of the content
|
// Generate a hash of the content
|
||||||
const hash = Password.hash(content);
|
const hash = Password.hash(content);
|
||||||
|
|
||||||
// Parse the PKCS#1 PEM key into a proper KeyObject
|
// Parse the PEM key into a proper KeyObject
|
||||||
const publicKey = crypto.createPublicKey({
|
const publicKey = crypto.createPublicKey(secureValuesPublicKey);
|
||||||
key: secureValuesPublicKey,
|
|
||||||
format: "pem",
|
|
||||||
type: "pkcs1",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Encrypt the content using the .secureValues.pub public key
|
// Encrypt the content using the .secureValues.pub public key
|
||||||
const encrypted = crypto.publicEncrypt(
|
const encrypted = crypto.publicEncrypt(
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import crypto from "crypto";
|
|||||||
import { secureValuesPrivateKey } from "../../constants";
|
import { secureValuesPrivateKey } from "../../constants";
|
||||||
import GenericError from "../../Errors/GenericError";
|
import GenericError from "../../Errors/GenericError";
|
||||||
|
|
||||||
const privateKey = crypto.createPrivateKey({
|
const privateKey = crypto.createPrivateKey(secureValuesPrivateKey);
|
||||||
key: secureValuesPrivateKey,
|
|
||||||
format: "pem",
|
|
||||||
type: "pkcs1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const readSecureValue = (
|
export const readSecureValue = (
|
||||||
encryptedContent: string,
|
encryptedContent: string,
|
||||||
|
|||||||
@@ -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 ✅");
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import keypair from "keypair";
|
import crypto from "crypto";
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
Generating Private Keys
|
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.
|
This process might take several minutes.
|
||||||
-----------------`);
|
-----------------`);
|
||||||
|
|
||||||
@@ -42,9 +42,13 @@ await Promise.all(
|
|||||||
if (!privExists || !pubExists) {
|
if (!privExists || !pubExists) {
|
||||||
// Always regenerate both files together to ensure the key pair matches
|
// Always regenerate both files together to ensure the key pair matches
|
||||||
console.log(`Generating '${v}' and '${pubPath}'...`);
|
console.log(`Generating '${v}' and '${pubPath}'...`);
|
||||||
const keys = keypair({ bits: 4096 });
|
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
|
||||||
await Bun.write(v, keys.private);
|
modulusLength: 4096,
|
||||||
await Bun.write(pubPath, keys.public);
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
});
|
||||||
|
await Bun.write(v, privateKey);
|
||||||
|
await Bun.write(pubPath, publicKey);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}),
|
}),
|
||||||
|
|||||||
+16
-11
@@ -1,4 +1,4 @@
|
|||||||
import keypair from "keypair";
|
import crypto from "crypto";
|
||||||
import { mkdirSync } from "fs";
|
import { mkdirSync } from "fs";
|
||||||
|
|
||||||
const outputDir = "production-keys";
|
const outputDir = "production-keys";
|
||||||
@@ -19,14 +19,18 @@ const generatedKeys: Record<string, { private: string; public: string }> = {};
|
|||||||
|
|
||||||
for (const name of keyFiles) {
|
for (const name of keyFiles) {
|
||||||
console.log(`Generating '${name}' key pair (4096-bit RSA)...`);
|
console.log(`Generating '${name}' key pair (4096-bit RSA)...`);
|
||||||
const keys = keypair({ bits: 4096 });
|
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
|
||||||
generatedKeys[name] = keys;
|
modulusLength: 4096,
|
||||||
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
});
|
||||||
|
generatedKeys[name] = { private: privateKey, public: publicKey };
|
||||||
|
|
||||||
const privPath = `${outputDir}/${name}.key`;
|
const privPath = `${outputDir}/${name}.key`;
|
||||||
const pubPath = `${outputDir}/${name}.pub`;
|
const pubPath = `${outputDir}/${name}.pub`;
|
||||||
|
|
||||||
await Bun.write(privPath, keys.private);
|
await Bun.write(privPath, privateKey);
|
||||||
await Bun.write(pubPath, keys.public);
|
await Bun.write(pubPath, publicKey);
|
||||||
|
|
||||||
console.log(` ✔ ${privPath}`);
|
console.log(` ✔ ${privPath}`);
|
||||||
console.log(` ✔ ${pubPath}`);
|
console.log(` ✔ ${pubPath}`);
|
||||||
@@ -38,14 +42,15 @@ const toBase64 = (str: string) => Buffer.from(str).toString("base64");
|
|||||||
const secretYaml = `apiVersion: v1
|
const secretYaml = `apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: optima-keys
|
name: optima-keys-secret
|
||||||
|
namespace: optima
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
accessToken.key: ${toBase64(generatedKeys["accessToken"].private)}
|
ACCESS_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["accessToken"].private)}
|
||||||
refreshToken.key: ${toBase64(generatedKeys["refreshToken"].private)}
|
REFRESH_TOKEN_PRIVATE_KEY: ${toBase64(generatedKeys["refreshToken"].private)}
|
||||||
permissions.key: ${toBase64(generatedKeys["permissions"].private)}
|
PERMISSIONS_PRIVATE_KEY: ${toBase64(generatedKeys["permissions"].private)}
|
||||||
secureValues.key: ${toBase64(generatedKeys["secureValues"].private)}
|
SECURE_VALUES_PRIVATE_KEY: ${toBase64(generatedKeys["secureValues"].private)}
|
||||||
secureValues.pub: ${toBase64(generatedKeys["secureValues"].public)}
|
SECURE_VALUES_PUBLIC_KEY: ${toBase64(generatedKeys["secureValues"].public)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const secretPath = `${outputDir}/optima-keys-secret.yaml`;
|
const secretPath = `${outputDir}/optima-keys-secret.yaml`;
|
||||||
|
|||||||
Reference in New Issue
Block a user