mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge branch 'dev' of github.com:fosrl/pangolin into dev
This commit is contained in:
15
docker-compose.drizzle.yml
Normal file
15
docker-compose.drizzle.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
drizzle-gateway:
|
||||
image: ghcr.io/drizzle-team/gateway:latest
|
||||
ports:
|
||||
- "4984:4983"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- STORE_PATH=/app
|
||||
- DATABASE_URL=postgresql://postgres:password@db:5432/postgres
|
||||
volumes:
|
||||
- drizzle-gateway-data:/app
|
||||
|
||||
volumes:
|
||||
drizzle-gateway-data:
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- ./config/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432" # Map host port 5432 to container port 5432
|
||||
restart: no
|
||||
restart: no
|
||||
|
||||
redis:
|
||||
image: redis:latest # Use the latest Redis image
|
||||
|
||||
@@ -1163,6 +1163,25 @@
|
||||
"sidebarLicense": "License",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domains",
|
||||
"sidebarBluePrints": "Blueprints",
|
||||
"blueprints": "Blueprints",
|
||||
"blueprintsDescription": "Blueprints are declarative YAML configurations that define your resources and their settings",
|
||||
"blueprintAdd": "Add Blueprint",
|
||||
"blueprintGoBack": "See all Blueprints",
|
||||
"blueprintCreate": "Create Blueprint",
|
||||
"blueprintCreateDescription2": "Follow the steps below to create and apply a new blueprint",
|
||||
"blueprintDetails": "Blueprint details",
|
||||
"blueprintDetailsDescription": "See the blueprint run details",
|
||||
"blueprintInfo": "Blueprint Information",
|
||||
"message": "Message",
|
||||
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
|
||||
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
||||
"blueprintErrorCreate": "Error creating blueprint",
|
||||
"searchBlueprintProgress": "Search blueprints...",
|
||||
"appliedAt": "Applied At",
|
||||
"source": "Source",
|
||||
"contents": "Contents",
|
||||
"parsedContents": "Parsed Contents",
|
||||
"enableDockerSocket": "Enable Docker Blueprint",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||
"enableDockerSocketLink": "Learn More",
|
||||
|
||||
@@ -7,7 +7,8 @@ const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true
|
||||
},
|
||||
output: "standalone"
|
||||
output: "standalone",
|
||||
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
114
package-lock.json
generated
114
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||
"@aws-sdk/client-s3": "3.908.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
@@ -73,10 +74,12 @@
|
||||
"next": "15.5.6",
|
||||
"next-intl": "^4.3.12",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.10",
|
||||
"npm": "^11.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.10.4",
|
||||
@@ -100,6 +103,7 @@
|
||||
"winston": "3.18.3",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "^2.8.1",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "3.25.76",
|
||||
"zod-validation-error": "3.5.2"
|
||||
@@ -107,6 +111,7 @@
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.51.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
@@ -120,6 +125,7 @@
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "24.9.2",
|
||||
"@types/nodemailer": "7.0.3",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/pg": "8.15.6",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
@@ -3104,6 +3110,23 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
|
||||
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
@@ -3849,6 +3872,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz",
|
||||
"integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
|
||||
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -8681,6 +8727,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/nprogress": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz",
|
||||
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.15.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
|
||||
@@ -11186,6 +11239,13 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
@@ -14975,6 +15035,30 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.54.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz",
|
||||
"integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.1.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor/node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
@@ -15202,6 +15286,24 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/nextjs-toploader": {
|
||||
"version": "3.9.17",
|
||||
"resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz",
|
||||
"integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nprogress": "^0.2.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://buymeacoffee.com/thesgj"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">= 6.0.0",
|
||||
"react": ">= 16.0.0",
|
||||
"react-dom": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.78.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
|
||||
@@ -17732,6 +17834,12 @@
|
||||
"inBundle": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/nprogress": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -20579,6 +20687,12 @@
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||
"@aws-sdk/client-s3": "3.908.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
@@ -96,10 +97,12 @@
|
||||
"next": "15.5.6",
|
||||
"next-intl": "^4.3.12",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.10",
|
||||
"npm": "^11.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.10.4",
|
||||
@@ -123,6 +126,7 @@
|
||||
"winston": "3.18.3",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "^2.8.1",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "3.25.76",
|
||||
"zod-validation-error": "3.5.2"
|
||||
@@ -130,6 +134,7 @@
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.51.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
@@ -141,6 +146,7 @@
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/node": "24.9.2",
|
||||
"@types/nodemailer": "7.0.3",
|
||||
"@types/pg": "8.15.6",
|
||||
|
||||
@@ -119,6 +119,8 @@ export enum ActionsEnum {
|
||||
updateLoginPage = "updateLoginPage",
|
||||
getLoginPage = "getLoginPage",
|
||||
deleteLoginPage = "deleteLoginPage",
|
||||
listBlueprints = "listBlueprints",
|
||||
getBlueprint = "getBlueprint",
|
||||
applyBlueprint = "applyBlueprint",
|
||||
viewLogs = "viewLogs",
|
||||
exportLogs = "exportLogs"
|
||||
@@ -198,7 +200,6 @@ export async function checkUserActionPermission(
|
||||
.limit(1);
|
||||
|
||||
return roleActionPermission.length > 0;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error checking user action permission:", error);
|
||||
throw createHttpError(
|
||||
|
||||
@@ -25,7 +25,6 @@ export const domains = pgTable("domains", {
|
||||
preferWildcardCert: boolean("preferWildcardCert")
|
||||
});
|
||||
|
||||
|
||||
export const dnsRecords = pgTable("dnsRecords", {
|
||||
id: serial("id").primaryKey(),
|
||||
domainId: varchar("domainId")
|
||||
@@ -34,7 +33,7 @@ export const dnsRecords = pgTable("dnsRecords", {
|
||||
recordType: varchar("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||
baseDomain: varchar("baseDomain"),
|
||||
value: varchar("value").notNull(),
|
||||
verified: boolean("verified").notNull().default(false),
|
||||
verified: boolean("verified").notNull().default(false)
|
||||
});
|
||||
|
||||
export const orgs = pgTable("orgs", {
|
||||
@@ -703,6 +702,21 @@ export const setupTokens = pgTable("setupTokens", {
|
||||
dateUsed: varchar("dateUsed")
|
||||
});
|
||||
|
||||
// Blueprint runs
|
||||
export const blueprints = pgTable("blueprints", {
|
||||
blueprintId: serial("blueprintId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
source: varchar("source").notNull(),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
succeeded: boolean("succeeded").notNull(),
|
||||
contents: text("contents").notNull(),
|
||||
message: text("message")
|
||||
});
|
||||
export const requestAuditLog = pgTable(
|
||||
"requestAuditLog",
|
||||
{
|
||||
@@ -790,6 +804,7 @@ export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
export type Blueprint = InferSelectModel<typeof blueprints>;
|
||||
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
|
||||
@@ -13,12 +13,17 @@ bootstrapVolume();
|
||||
|
||||
function createDb() {
|
||||
const sqlite = new Database(location);
|
||||
return DrizzleSqlite(sqlite, { schema });
|
||||
return DrizzleSqlite(sqlite, {
|
||||
schema,
|
||||
logger: process.env.NODE_ENV === "development"
|
||||
});
|
||||
}
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
|
||||
function checkFileExists(filePath: string): boolean {
|
||||
try {
|
||||
|
||||
@@ -749,6 +749,23 @@ export const idpOrg = sqliteTable("idpOrg", {
|
||||
orgMapping: text("orgMapping")
|
||||
});
|
||||
|
||||
// Blueprint runs
|
||||
export const blueprints = sqliteTable("blueprints", {
|
||||
blueprintId: integer("blueprintId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: text("name").notNull(),
|
||||
source: text("source").notNull(),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
succeeded: integer("succeeded", { mode: "boolean" }).notNull(),
|
||||
contents: text("contents").notNull(),
|
||||
message: text("message")
|
||||
});
|
||||
export const requestAuditLog = sqliteTable(
|
||||
"requestAuditLog",
|
||||
{
|
||||
@@ -837,6 +854,7 @@ export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
export type Blueprint = InferSelectModel<typeof blueprints>;
|
||||
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import { db, newts, Target } from "@server/db";
|
||||
import { db, newts, blueprints, Blueprint } from "@server/db";
|
||||
import { Config, ConfigSchema } from "./types";
|
||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { resources, targets, sites } from "@server/db";
|
||||
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||
import { sites } from "@server/db";
|
||||
import { eq, and, isNotNull } from "drizzle-orm";
|
||||
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
|
||||
import { addTargets as addClientTargets } from "@server/routers/client/targets";
|
||||
import {
|
||||
ClientResourcesResults,
|
||||
updateClientResources
|
||||
} from "./clientResources";
|
||||
import { BlueprintSource } from "@server/routers/blueprints/types";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export async function applyBlueprint(
|
||||
orgId: string,
|
||||
configData: unknown,
|
||||
siteId?: number
|
||||
): Promise<void> {
|
||||
type ApplyBlueprintArgs = {
|
||||
orgId: string;
|
||||
configData: unknown;
|
||||
name?: string;
|
||||
siteId?: number;
|
||||
source?: BlueprintSource;
|
||||
};
|
||||
|
||||
export async function applyBlueprint({
|
||||
orgId,
|
||||
configData,
|
||||
siteId,
|
||||
name,
|
||||
source = "API"
|
||||
}: ApplyBlueprintArgs): Promise<Blueprint> {
|
||||
// Validate the input data
|
||||
const validationResult = ConfigSchema.safeParse(configData);
|
||||
if (!validationResult.success) {
|
||||
@@ -24,6 +37,9 @@ export async function applyBlueprint(
|
||||
}
|
||||
|
||||
const config: Config = validationResult.data;
|
||||
let blueprintSucceeded: boolean = false;
|
||||
let blueprintMessage: string;
|
||||
let error: any | null = null;
|
||||
|
||||
try {
|
||||
let proxyResourcesResults: ProxyResourcesResults = [];
|
||||
@@ -120,10 +136,42 @@ export async function applyBlueprint(
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update database from config: ${error}`);
|
||||
throw error;
|
||||
|
||||
blueprintSucceeded = true;
|
||||
blueprintMessage = "Blueprint applied successfully";
|
||||
} catch (err) {
|
||||
blueprintSucceeded = false;
|
||||
blueprintMessage = `Blueprint applied with errors: ${err}`;
|
||||
logger.error(blueprintMessage);
|
||||
error = err;
|
||||
}
|
||||
|
||||
let blueprint: Blueprint | null = null;
|
||||
await db.transaction(async (trx) => {
|
||||
const newBlueprint = await trx
|
||||
.insert(blueprints)
|
||||
.values({
|
||||
orgId,
|
||||
name:
|
||||
name ??
|
||||
`${faker.word.adjective()} ${faker.word.adjective()} ${faker.word.noun()}`,
|
||||
contents: stringifyYaml(configData),
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
succeeded: blueprintSucceeded,
|
||||
message: blueprintMessage,
|
||||
source
|
||||
})
|
||||
.returning();
|
||||
|
||||
blueprint = newBlueprint[0];
|
||||
});
|
||||
|
||||
if (!blueprint || (source !== "UI" && !blueprintSucceeded)) {
|
||||
// ^^^^^^^^^^^^^^^ The UI considers a failed blueprint as a valid response
|
||||
throw error ?? "Unknown Server Error";
|
||||
}
|
||||
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
// await updateDatabaseFromConfig("org_i21aifypnlyxur2", {
|
||||
|
||||
@@ -30,7 +30,12 @@ export async function applyNewtDockerBlueprint(
|
||||
logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
|
||||
|
||||
// Update the blueprint in the database
|
||||
await applyBlueprint(site.orgId, blueprint, site.siteId);
|
||||
await applyBlueprint({
|
||||
orgId: site.orgId,
|
||||
configData: blueprint,
|
||||
siteId: site.siteId,
|
||||
source: "NEWT"
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update database from config: ${error}`);
|
||||
await sendToClient(newtId, {
|
||||
|
||||
@@ -9,7 +9,7 @@ const nextPort = config.getRawConfig().server.next_port;
|
||||
|
||||
export async function createNextServer() {
|
||||
// const app = next({ dev });
|
||||
const app = next({ dev: process.env.ENVIRONMENT !== "prod" });
|
||||
const app = next({ dev: process.env.ENVIRONMENT !== "prod", turbopack: true });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
|
||||
@@ -15,5 +15,6 @@ export enum OpenAPITags {
|
||||
Idp = "Identity Provider",
|
||||
Client = "Client",
|
||||
ApiKey = "API Key",
|
||||
Domain = "Domain"
|
||||
Domain = "Domain",
|
||||
Blueprint = "Blueprint"
|
||||
}
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
apiKeyOrg,
|
||||
apiKeys,
|
||||
domains,
|
||||
Org,
|
||||
orgDomains,
|
||||
orgs,
|
||||
roleActions,
|
||||
roles,
|
||||
userOrgs,
|
||||
users,
|
||||
actions
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { defaultRoleAllowedActions } from "../role";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isValidCIDR } from "@server/lib/validators";
|
||||
import { applyBlueprint as applyBlueprintFunc } from "@server/lib/blueprints/applyBlueprint";
|
||||
import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint";
|
||||
|
||||
const applyBlueprintSchema = z
|
||||
.object({
|
||||
@@ -41,8 +23,8 @@ const applyBlueprintParamsSchema = z
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/blueprint",
|
||||
description: "Apply a base64 encoded blueprint to an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
description: "Apply a base64 encoded JSON blueprint to an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: applyBlueprintParamsSchema,
|
||||
body: {
|
||||
@@ -56,7 +38,7 @@ registry.registerPath({
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function applyBlueprint(
|
||||
export async function applyJSONBlueprint(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -100,7 +82,11 @@ export async function applyBlueprint(
|
||||
const blueprintParsed = JSON.parse(decoded);
|
||||
|
||||
// Update the blueprint in the database
|
||||
await applyBlueprintFunc(orgId, blueprintParsed);
|
||||
await applyBlueprint({
|
||||
orgId,
|
||||
configData: blueprintParsed,
|
||||
source: "API"
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update database from config: ${error}`);
|
||||
return next(
|
||||
146
server/routers/blueprints/applyYAMLBlueprint.ts
Normal file
146
server/routers/blueprints/applyYAMLBlueprint.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import z from "zod";
|
||||
import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import logger from "@server/logger";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import response from "@server/lib/response";
|
||||
import { type Blueprint } from "@server/db";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { ConfigSchema } from "@server/lib/blueprints/types";
|
||||
|
||||
const applyBlueprintSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
blueprint: z
|
||||
.string()
|
||||
.min(1)
|
||||
.superRefine((val, ctx) => {
|
||||
try {
|
||||
parseYaml(val);
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.strict();
|
||||
|
||||
const applyBlueprintParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateBlueprintResponse = Blueprint;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/blueprint",
|
||||
description: "Create and apply a YAML blueprint to an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: applyBlueprintParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: applyBlueprintSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function applyYAMLBlueprint(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = applyBlueprintParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const parsedBody = applyBlueprintSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedBody.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { blueprint: contents, name } = parsedBody.data;
|
||||
|
||||
logger.debug(`Received blueprint:`, contents);
|
||||
|
||||
const parsedConfig = parseYaml(contents);
|
||||
// apply the validation in advance so that error concerning the format are ruled out first
|
||||
const validationResult = ConfigSchema.safeParse(parsedConfig);
|
||||
if (!validationResult.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(validationResult.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let blueprint: Blueprint | null = null;
|
||||
|
||||
let error: string | null = null;
|
||||
try {
|
||||
blueprint = await applyBlueprint({
|
||||
orgId,
|
||||
name,
|
||||
source: "UI",
|
||||
configData: parsedConfig
|
||||
});
|
||||
} catch (err) {
|
||||
// We do nothing, the error is thrown for the other APIs & websockets for backwards compatibility
|
||||
// for this API, the error is already saved in the blueprint and we don't need to handle it
|
||||
logger.error(err);
|
||||
if (err instanceof Error) {
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (!blueprint) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
error
|
||||
? error
|
||||
: "An unknown error occurred while applying the blueprint"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: blueprint,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Done",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
110
server/routers/blueprints/getBlueprint.ts
Normal file
110
server/routers/blueprints/getBlueprint.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { blueprints, orgs } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { BlueprintData } from "./types";
|
||||
|
||||
const getBlueprintSchema = z
|
||||
.object({
|
||||
blueprintId: z
|
||||
.string()
|
||||
.transform(stoi)
|
||||
.pipe(z.number().int().positive()),
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function query(blueprintId: number, orgId: string) {
|
||||
// Get the client
|
||||
const [blueprint] = await db
|
||||
.select({
|
||||
blueprintId: blueprints.blueprintId,
|
||||
name: blueprints.name,
|
||||
source: blueprints.source,
|
||||
succeeded: blueprints.succeeded,
|
||||
orgId: blueprints.orgId,
|
||||
createdAt: blueprints.createdAt,
|
||||
message: blueprints.message,
|
||||
contents: blueprints.contents
|
||||
})
|
||||
.from(blueprints)
|
||||
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
||||
.where(
|
||||
and(
|
||||
eq(blueprints.blueprintId, blueprintId),
|
||||
eq(blueprints.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!blueprint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
export type GetBlueprintResponse = BlueprintData;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/blueprint/{blueprintId}",
|
||||
description: "Get a blueprint by its blueprint ID.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: getBlueprintSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getBlueprint(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getBlueprintSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
logger.error(
|
||||
`Error parsing params: ${fromError(parsedParams.error).toString()}`
|
||||
);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, blueprintId } = parsedParams.data;
|
||||
|
||||
const blueprint = await query(blueprintId, orgId);
|
||||
|
||||
if (!blueprint) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Client not found")
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetBlueprintResponse>(res, {
|
||||
data: blueprint as BlueprintData,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
4
server/routers/blueprints/index.ts
Normal file
4
server/routers/blueprints/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./listBlueprints";
|
||||
export * from "./applyYAMLBlueprint";
|
||||
export * from "./applyJSONBlueprint";
|
||||
export * from "./getBlueprint";
|
||||
144
server/routers/blueprints/listBlueprints.ts
Normal file
144
server/routers/blueprints/listBlueprints.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, blueprints, orgs } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, eq, desc } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { BlueprintData } from "./types";
|
||||
|
||||
const listBluePrintsParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const listBluePrintsSchema = z
|
||||
.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function queryBlueprints(orgId: string, limit: number, offset: number) {
|
||||
const res = await db
|
||||
.select({
|
||||
blueprintId: blueprints.blueprintId,
|
||||
name: blueprints.name,
|
||||
source: blueprints.source,
|
||||
succeeded: blueprints.succeeded,
|
||||
orgId: blueprints.orgId,
|
||||
createdAt: blueprints.createdAt
|
||||
})
|
||||
.from(blueprints)
|
||||
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
||||
.where(eq(blueprints.orgId, orgId))
|
||||
.orderBy(desc(blueprints.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return res;
|
||||
}
|
||||
|
||||
export type ListBlueprintsResponse = {
|
||||
blueprints: NonNullable<
|
||||
Pick<
|
||||
BlueprintData,
|
||||
| "blueprintId"
|
||||
| "name"
|
||||
| "source"
|
||||
| "succeeded"
|
||||
| "orgId"
|
||||
| "createdAt"
|
||||
>[]
|
||||
>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/blueprints",
|
||||
description: "List all blueprints for a organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listBluePrintsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listBlueprints(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listBluePrintsSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listBluePrintsParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const blueprintsList = await queryBlueprints(
|
||||
orgId.toString(),
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(blueprints);
|
||||
|
||||
return response<ListBlueprintsResponse>(res, {
|
||||
data: {
|
||||
blueprints:
|
||||
blueprintsList as ListBlueprintsResponse["blueprints"],
|
||||
pagination: {
|
||||
total: count,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Blueprints retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
7
server/routers/blueprints/types.ts
Normal file
7
server/routers/blueprints/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Blueprint } from "@server/db";
|
||||
|
||||
export type BlueprintSource = "API" | "UI" | "NEWT";
|
||||
|
||||
export type BlueprintData = Omit<Blueprint, "source"> & {
|
||||
source: BlueprintSource;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import * as siteResource from "./siteResource";
|
||||
import * as supporterKey from "./supporterKey";
|
||||
import * as accessToken from "./accessToken";
|
||||
import * as idp from "./idp";
|
||||
import * as blueprints from "./blueprints";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as logs from "./auditLogs";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -895,6 +896,27 @@ authenticated.get(
|
||||
logs.exportRequestAuditLogs
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/blueprints",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listBlueprints),
|
||||
blueprints.listBlueprints
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/blueprint",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.applyBlueprint),
|
||||
blueprints.applyYAMLBlueprint
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/blueprint/:blueprintId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getBlueprint),
|
||||
blueprints.getBlueprint
|
||||
);
|
||||
|
||||
// Auth routes
|
||||
export const authRouter = Router();
|
||||
unauthenticated.use("/auth", authRouter);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as site from "./site";
|
||||
import * as org from "./org";
|
||||
import * as blueprints from "./blueprints";
|
||||
import * as resource from "./resource";
|
||||
import * as domain from "./domain";
|
||||
import * as target from "./target";
|
||||
@@ -52,7 +53,7 @@ authenticated.put(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrg),
|
||||
logActionAudit(ActionsEnum.createOrg),
|
||||
org.createOrg,
|
||||
org.createOrg
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -74,7 +75,7 @@ authenticated.post(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrg),
|
||||
logActionAudit(ActionsEnum.updateOrg),
|
||||
org.updateOrg,
|
||||
org.updateOrg
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -82,7 +83,7 @@ authenticated.delete(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteOrg),
|
||||
logActionAudit(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg,
|
||||
org.deleteOrg
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -90,7 +91,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
||||
logActionAudit(ActionsEnum.createSite),
|
||||
site.createSite,
|
||||
site.createSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -126,7 +127,7 @@ authenticated.post(
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSite),
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite,
|
||||
site.updateSite
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -134,7 +135,7 @@ authenticated.delete(
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSite),
|
||||
logActionAudit(ActionsEnum.deleteSite),
|
||||
site.deleteSite,
|
||||
site.deleteSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -149,7 +150,7 @@ authenticated.put(
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||
logActionAudit(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource,
|
||||
siteResource.createSiteResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -183,7 +184,7 @@ authenticated.post(
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||
logActionAudit(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource,
|
||||
siteResource.updateSiteResource
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -193,7 +194,7 @@ authenticated.delete(
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||
siteResource.deleteSiteResource,
|
||||
siteResource.deleteSiteResource
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -201,7 +202,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource,
|
||||
resource.createResource
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -209,7 +210,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource,
|
||||
resource.createResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -245,7 +246,7 @@ authenticated.post(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.inviteUser),
|
||||
logActionAudit(ActionsEnum.inviteUser),
|
||||
user.inviteUser,
|
||||
user.inviteUser
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -274,7 +275,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
logActionAudit(ActionsEnum.updateResource),
|
||||
resource.updateResource,
|
||||
resource.updateResource
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -282,7 +283,7 @@ authenticated.delete(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResource),
|
||||
logActionAudit(ActionsEnum.deleteResource),
|
||||
resource.deleteResource,
|
||||
resource.deleteResource
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -290,7 +291,7 @@ authenticated.put(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
||||
logActionAudit(ActionsEnum.createTarget),
|
||||
target.createTarget,
|
||||
target.createTarget
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -305,7 +306,7 @@ authenticated.put(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
||||
logActionAudit(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule,
|
||||
resource.createResourceRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -320,7 +321,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
||||
logActionAudit(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule,
|
||||
resource.updateResourceRule
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -328,7 +329,7 @@ authenticated.delete(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
|
||||
logActionAudit(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule,
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -343,7 +344,7 @@ authenticated.post(
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateTarget),
|
||||
logActionAudit(ActionsEnum.updateTarget),
|
||||
target.updateTarget,
|
||||
target.updateTarget
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -351,7 +352,7 @@ authenticated.delete(
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteTarget),
|
||||
logActionAudit(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget,
|
||||
target.deleteTarget
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -359,7 +360,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createRole),
|
||||
logActionAudit(ActionsEnum.createRole),
|
||||
role.createRole,
|
||||
role.createRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -374,7 +375,7 @@ authenticated.delete(
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteRole),
|
||||
logActionAudit(ActionsEnum.deleteRole),
|
||||
role.deleteRole,
|
||||
role.deleteRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -390,7 +391,7 @@ authenticated.post(
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||
logActionAudit(ActionsEnum.addUserRole),
|
||||
user.addUserRole,
|
||||
user.addUserRole
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -399,7 +400,7 @@ authenticated.post(
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles,
|
||||
resource.setResourceRoles
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -408,7 +409,7 @@ authenticated.post(
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers,
|
||||
resource.setResourceUsers
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -416,7 +417,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
||||
logActionAudit(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword,
|
||||
resource.setResourcePassword
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -424,7 +425,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
||||
logActionAudit(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode,
|
||||
resource.setResourcePincode
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -432,7 +433,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth,
|
||||
resource.setResourceHeaderAuth
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -440,7 +441,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist,
|
||||
resource.setResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -469,7 +470,7 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
||||
logActionAudit(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken,
|
||||
accessToken.generateAccessToken
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -477,7 +478,7 @@ authenticated.delete(
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteAcessToken),
|
||||
logActionAudit(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken,
|
||||
accessToken.deleteAccessToken
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -506,7 +507,7 @@ authenticated.post(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateUser),
|
||||
logActionAudit(ActionsEnum.updateUser),
|
||||
user.updateUser2FA,
|
||||
user.updateUser2FA
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -528,7 +529,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
|
||||
logActionAudit(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser,
|
||||
user.createOrgUser
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -537,7 +538,7 @@ authenticated.post(
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
|
||||
logActionAudit(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser,
|
||||
user.updateOrgUser
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -546,7 +547,7 @@ authenticated.delete(
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.removeUser),
|
||||
logActionAudit(ActionsEnum.removeUser),
|
||||
user.removeUserOrg,
|
||||
user.removeUserOrg
|
||||
);
|
||||
|
||||
// authenticated.put(
|
||||
@@ -567,7 +568,7 @@ authenticated.post(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
|
||||
logActionAudit(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions,
|
||||
apiKeys.setApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -582,7 +583,7 @@ authenticated.put(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createApiKey),
|
||||
logActionAudit(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey,
|
||||
apiKeys.createOrgApiKey
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -590,7 +591,7 @@ authenticated.delete(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteApiKey),
|
||||
logActionAudit(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteApiKey,
|
||||
apiKeys.deleteApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -598,7 +599,7 @@ authenticated.put(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp,
|
||||
idp.createOidcIdp
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -606,7 +607,7 @@ authenticated.post(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
idp.updateOidcIdp,
|
||||
idp.updateOidcIdp
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -628,7 +629,7 @@ authenticated.put(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
|
||||
logActionAudit(ActionsEnum.createIdpOrg),
|
||||
idp.createIdpOrgPolicy,
|
||||
idp.createIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -636,7 +637,7 @@ authenticated.post(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
|
||||
logActionAudit(ActionsEnum.updateIdpOrg),
|
||||
idp.updateIdpOrgPolicy,
|
||||
idp.updateIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -644,7 +645,7 @@ authenticated.delete(
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg),
|
||||
logActionAudit(ActionsEnum.deleteIdpOrg),
|
||||
idp.deleteIdpOrgPolicy,
|
||||
idp.deleteIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -684,7 +685,7 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createClient),
|
||||
logActionAudit(ActionsEnum.createClient),
|
||||
client.createClient,
|
||||
client.createClient
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -693,7 +694,7 @@ authenticated.delete(
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteClient),
|
||||
logActionAudit(ActionsEnum.deleteClient),
|
||||
client.deleteClient,
|
||||
client.deleteClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -702,7 +703,7 @@ authenticated.post(
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||
logActionAudit(ActionsEnum.updateClient),
|
||||
client.updateClient,
|
||||
client.updateClient
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
@@ -710,5 +711,5 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
|
||||
logActionAudit(ActionsEnum.applyBlueprint),
|
||||
org.applyBlueprint,
|
||||
blueprints.applyJSONBlueprint
|
||||
);
|
||||
|
||||
@@ -43,7 +43,12 @@ export const handleApplyBlueprintMessage: MessageHandler = async (context) => {
|
||||
try {
|
||||
const blueprintParsed = JSON.parse(blueprint);
|
||||
// Update the blueprint in the database
|
||||
await applyBlueprint(site.orgId, blueprintParsed, site.siteId);
|
||||
await applyBlueprint({
|
||||
orgId: site.orgId,
|
||||
configData: blueprintParsed,
|
||||
siteId: site.siteId,
|
||||
source: "NEWT"
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update database from config: ${error}`);
|
||||
return {
|
||||
|
||||
@@ -7,5 +7,4 @@ export * from "./checkId";
|
||||
export * from "./getOrgOverview";
|
||||
export * from "./listOrgs";
|
||||
export * from "./pickOrgDefaults";
|
||||
export * from "./applyBlueprint";
|
||||
export * from "./checkOrgUserAccess";
|
||||
|
||||
58
src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx
Normal file
58
src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import BlueprintDetailsForm from "@app/components/BlueprintDetailsForm";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import { GetBlueprintResponse } from "@server/routers/blueprints";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
type BluePrintsPageProps = {
|
||||
params: Promise<{ orgId: string; blueprintId: string }>;
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blueprint Detail"
|
||||
};
|
||||
|
||||
export default async function BluePrintDetailPage(props: BluePrintsPageProps) {
|
||||
const params = await props.params;
|
||||
let org = null;
|
||||
try {
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
}
|
||||
|
||||
let blueprint = null;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetBlueprintResponse>>(
|
||||
`/org/${params.orgId}/blueprint/${params.blueprintId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
|
||||
blueprint = res.data.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("blueprintDetails")}
|
||||
description={t("blueprintDetailsDescription")}
|
||||
/>
|
||||
|
||||
<BlueprintDetailsForm blueprint={blueprint} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
42
src/app/[orgId]/settings/blueprints/create/page.tsx
Normal file
42
src/app/[orgId]/settings/blueprints/create/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import CreateBlueprintForm from "@app/components/CreateBlueprintForm";
|
||||
|
||||
export interface CreateBlueprintPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create blueprint"
|
||||
};
|
||||
|
||||
export default async function CreateBlueprintPage(
|
||||
props: CreateBlueprintPageProps
|
||||
) {
|
||||
const t = await getTranslations();
|
||||
|
||||
const orgId = (await props.params).orgId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2 justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={t("blueprintCreate")}
|
||||
description={t("blueprintCreateDescription2")}
|
||||
/>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/${orgId}/settings/blueprints`}>
|
||||
{t("blueprintGoBack")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CreateBlueprintForm orgId={orgId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
57
src/app/[orgId]/settings/blueprints/page.tsx
Normal file
57
src/app/[orgId]/settings/blueprints/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import BlueprintsTable, {
|
||||
type BlueprintRow
|
||||
} from "@app/components/BlueprintsTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
type BluePrintsPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blueprints"
|
||||
};
|
||||
|
||||
export default async function BluePrintsPage(props: BluePrintsPageProps) {
|
||||
const params = await props.params;
|
||||
|
||||
let blueprints: BlueprintRow[] = [];
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<ListBlueprintsResponse>>(
|
||||
`/org/${params.orgId}/blueprints`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
|
||||
blueprints = res.data.data.blueprints;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<OrgProvider org={org}>
|
||||
<SettingsSectionTitle
|
||||
title={t("blueprints")}
|
||||
description={t("blueprintsDescription")}
|
||||
/>
|
||||
<BlueprintsTable blueprints={blueprints} orgId={params.orgId} />
|
||||
</OrgProvider>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||
import { redirect } from "next/navigation";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
import { toUnicode } from 'punycode';
|
||||
import { toUnicode } from "punycode";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -20,15 +21,16 @@ export default async function DomainsPage(props: Props) {
|
||||
|
||||
let domains: DomainRow[] = [];
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<ListDomainsResponse>
|
||||
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
||||
const res = await internal.get<AxiosResponse<ListDomainsResponse>>(
|
||||
`/org/${params.orgId}/domains`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
|
||||
domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain),
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -36,21 +38,12 @@ export default async function DomainsPage(props: Props) {
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${params.orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { Metadata } from "next";
|
||||
import {
|
||||
Combine,
|
||||
KeyRound,
|
||||
LinkIcon,
|
||||
Settings,
|
||||
Users,
|
||||
Waypoints,
|
||||
Workflow
|
||||
} from "lucide-react";
|
||||
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import { redirect } from "next/navigation";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ListOrgsResponse } from "@server/routers/org";
|
||||
import { GetOrgResponse, ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { cache } from "react";
|
||||
import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import UserProvider from "@app/providers/UserProvider";
|
||||
import { Layout } from "@app/components/Layout";
|
||||
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { orgNavSections } from "@app/app/navigation";
|
||||
@@ -27,7 +17,10 @@ import { orgNavSections } from "@app/app/navigation";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
||||
title: {
|
||||
template: `%s - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
||||
default: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}`
|
||||
},
|
||||
description: ""
|
||||
};
|
||||
|
||||
@@ -86,7 +79,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
return (
|
||||
<UserProvider user={user}>
|
||||
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections(env.flags.enableClients)}>
|
||||
<Layout
|
||||
orgId={params.orgId}
|
||||
orgs={orgs}
|
||||
navItems={orgNavSections(env.flags.enableClients)}
|
||||
>
|
||||
{children}
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
|
||||
17
src/app/[orgId]/settings/not-found.tsx
Normal file
17
src/app/[orgId]/settings/not-found.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function NotFound() {
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md mx-auto p-3 md:mt-32 text-center">
|
||||
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||
<h2 className="text-2xl font-semibold text-neutral-500 mb-4">
|
||||
{t("pageNotFound")}
|
||||
</h2>
|
||||
<p className="text-neutral-500 dark:text-neutral-700 mb-8">
|
||||
{t("pageNotFoundDescription")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.20 0.006 285.885);
|
||||
--background: oklch(0.2 0.006 285.885);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
@@ -140,3 +140,7 @@ p {
|
||||
word-break: keep-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { Toaster } from "@app/components/ui/toaster";
|
||||
import { build } from "@server/build";
|
||||
import { TopLoader } from "@app/components/Toploader";
|
||||
import Script from "next/script";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -85,6 +86,7 @@ export default async function RootLayout({
|
||||
return (
|
||||
<html suppressHydrationWarning lang={locale}>
|
||||
<body className={`${font.className} h-screen overflow-hidden`}>
|
||||
<TopLoader />
|
||||
{build === "saas" && (
|
||||
<Script
|
||||
src="https://rybbit.fossorial.io/api/script.js"
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { SidebarNavItem } from "@app/components/SidebarNav";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
Home,
|
||||
Settings,
|
||||
Users,
|
||||
Link as LinkIcon,
|
||||
Waypoints,
|
||||
Combine,
|
||||
Fingerprint,
|
||||
Workflow,
|
||||
KeyRound,
|
||||
TicketCheck,
|
||||
User,
|
||||
Globe, // Added from 'dev' branch
|
||||
MonitorUp, // Added from 'dev' branch
|
||||
Server,
|
||||
Zap,
|
||||
ReceiptText,
|
||||
CreditCard,
|
||||
Logs,
|
||||
SquareMousePointer,
|
||||
@@ -77,6 +75,11 @@ export const orgNavSections = (
|
||||
title: "sidebarDomains",
|
||||
href: "/{orgId}/settings/domains",
|
||||
icon: <Globe className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "sidebarBluePrints",
|
||||
href: "/{orgId}/settings/blueprints",
|
||||
icon: <ReceiptText className="h-4 w-4" />
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
208
src/components/BlueprintDetailsForm.tsx
Normal file
208
src/components/BlueprintDetailsForm.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "./ui/input";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import type { GetBlueprintResponse } from "@server/routers/blueprints";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "./InfoSection";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Globe, Terminal, Webhook } from "lucide-react";
|
||||
|
||||
export type CreateBlueprintFormProps = {
|
||||
blueprint: GetBlueprintResponse;
|
||||
};
|
||||
|
||||
export default function BlueprintDetailsForm({
|
||||
blueprint
|
||||
}: CreateBlueprintFormProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm({
|
||||
disabled: true,
|
||||
defaultValues: {
|
||||
name: blueprint.name,
|
||||
contents: blueprint.contents
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("appliedAt")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<time
|
||||
className="text-muted-foreground"
|
||||
dateTime={blueprint.createdAt.toString()}
|
||||
>
|
||||
{new Date(
|
||||
blueprint.createdAt * 1000
|
||||
).toLocaleString()}
|
||||
</time>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("status")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{blueprint.succeeded ? (
|
||||
<Badge variant="green">
|
||||
{t("success")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="red">
|
||||
{t("failed", {
|
||||
fallback: "Failed"
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("message")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<p className="text-muted-foreground">
|
||||
{blueprint.message}
|
||||
</p>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("source")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{blueprint.source === "API" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="-mx-2"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
API
|
||||
<Webhook className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{blueprint.source === "NEWT" && (
|
||||
<Badge variant="secondary">
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Newt CLI
|
||||
<Terminal className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{blueprint.source === "UI" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="-mx-1 py-1"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Dashboard{" "}
|
||||
<Globe className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
)}{" "}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("blueprintInfo")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm className="max-w-2xl">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contents"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("parsedContents")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"blueprintContentsDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<div
|
||||
className={cn(
|
||||
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
className="w-full h-full max-w-full"
|
||||
language="yaml"
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
readOnly: true
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
206
src/components/BlueprintsTable.tsx
Normal file
206
src/components/BlueprintsTable.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
Globe,
|
||||
Terminal,
|
||||
Webhook
|
||||
} from "lucide-react";
|
||||
import { useTransition } from "react";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DataTable } from "./ui/data-table";
|
||||
import Link from "next/link";
|
||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||
|
||||
export type BlueprintRow = ListBlueprintsResponse["blueprints"][number];
|
||||
|
||||
type Props = {
|
||||
blueprints: BlueprintRow[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
|
||||
const columns: ColumnDef<BlueprintRow>[] = [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("appliedAt")}
|
||||
<ArrowUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<time
|
||||
className="text-muted-foreground"
|
||||
dateTime={row.original.createdAt.toString()}
|
||||
>
|
||||
{new Date(
|
||||
row.original.createdAt * 1000
|
||||
).toLocaleString()}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "source",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("source")}
|
||||
<ArrowUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
switch (originalRow.source) {
|
||||
case "API": {
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
API
|
||||
<Webhook className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
case "NEWT": {
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Newt CLI
|
||||
<Terminal className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
case "UI": {
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Dashboard{" "}
|
||||
<Globe className="size-4 flex-none" />
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "succeeded",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("status")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { succeeded } = row.original;
|
||||
if (succeeded) {
|
||||
return <Badge variant="green">{t("success")}</Badge>;
|
||||
} else {
|
||||
return (
|
||||
<Badge variant="red">
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => {
|
||||
return null;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="items-center"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||
>
|
||||
View details{" "}
|
||||
<ArrowRight className="size-4 flex-none" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={blueprints}
|
||||
persistPageSize="blueprint-table"
|
||||
title={t("blueprints")}
|
||||
searchPlaceholder={t("searchBlueprintProgress")}
|
||||
searchColumn="name"
|
||||
onAdd={() => {
|
||||
router.push(`/${orgId}/settings/blueprints/create`);
|
||||
}}
|
||||
addButtonText={t("blueprintAdd")}
|
||||
onRefresh={() => {
|
||||
startTransition(() => router.refresh());
|
||||
}}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
id: "createdAt",
|
||||
desc: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
194
src/components/CreateBlueprintForm.tsx
Normal file
194
src/components/CreateBlueprintForm.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "./ui/input";
|
||||
import { useActionState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { Button } from "./ui/button";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import type { CreateBlueprintResponse } from "@server/routers/blueprints";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export type CreateBlueprintFormProps = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function CreateBlueprintForm({
|
||||
orgId
|
||||
}: CreateBlueprintFormProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
contents: z
|
||||
.string()
|
||||
.min(1)
|
||||
.superRefine((contents, ctx) => {
|
||||
try {
|
||||
parseYaml(contents);
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
contents: `# Example blueprint
|
||||
# proxy-resources:
|
||||
# resource-nice-id-uno:
|
||||
# name: this is my resource
|
||||
# protocol: http
|
||||
# full-domain: never-gonna-give-you-up.example.com
|
||||
# targets:
|
||||
# - site: lively-yosemite-toad
|
||||
# hostname: localhost
|
||||
# method: http
|
||||
# port: 8000
|
||||
`
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
const res = await api
|
||||
.put<AxiosResponse<CreateBlueprintResponse>>(
|
||||
`/org/${orgId}/blueprint/`,
|
||||
{
|
||||
name: form.getValues("name"),
|
||||
blueprint: form.getValues("contents")
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorCreate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("blueprintErrorCreateDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 201) {
|
||||
const createdBlueprint = res.data.data;
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: createdBlueprint.succeeded ? "Success" : "Warning",
|
||||
description: createdBlueprint.message
|
||||
});
|
||||
router.push(`/${orgId}/settings/blueprints`);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form action={formAction} id="base-resource-form">
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("blueprintInfo")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm className="max-w-2xl">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contents"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("contents")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"blueprintContentsDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<div
|
||||
className={cn(
|
||||
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
className="w-full h-full max-w-full"
|
||||
language="yaml"
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{t("actionApplyBlueprint")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
export function InfoSections({
|
||||
children,
|
||||
cols
|
||||
@@ -9,25 +11,44 @@ export function InfoSections({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`grid md:grid-cols-${cols || 1} md:gap-4 gap-2 md:items-start grid-cols-1`}
|
||||
className={`grid md:grid-cols-[var(--columns)] md:gap-4 gap-2 md:items-start grid-cols-1`}
|
||||
style={{
|
||||
// @ts-expect-error dynamic props don't work with tailwind, but we can set the
|
||||
// value of a CSS variable at runtime and tailwind will just reuse that value
|
||||
"--columns": `repeat(${cols || 1}, minmax(0, 1fr))`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoSection({ children }: { children: React.ReactNode }) {
|
||||
return <div className="space-y-1">{children}</div>;
|
||||
export function InfoSection({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-1", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function InfoSectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return <div className="font-semibold">{children}</div>;
|
||||
export function InfoSectionTitle({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("font-semibold", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function InfoSectionContent({
|
||||
children
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className="break-words">{children}</div>;
|
||||
return <div className={cn("break-words", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||
import { toUnicode } from "punycode";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
export function SettingsContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="space-y-6">{children}</div>;
|
||||
}
|
||||
|
||||
export function SettingsSection({ children }: { children: React.ReactNode }) {
|
||||
return <div className="border rounded-lg bg-card p-5 flex flex-col min-h-[200px]">{children}</div>;
|
||||
return (
|
||||
<div className="border rounded-lg bg-card p-5 flex flex-col min-h-[200px]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSectionHeader({
|
||||
@@ -15,11 +21,15 @@ export function SettingsSectionHeader({
|
||||
}
|
||||
|
||||
export function SettingsSectionForm({
|
||||
children
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className="max-w-xl space-y-4">{children}</div>;
|
||||
return (
|
||||
<div className={cn("max-w-xl space-y-4", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSectionTitle({
|
||||
@@ -55,7 +65,11 @@ export function SettingsSectionFooter({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <div className="flex justify-end space-x-2 mt-auto pt-6">{children}</div>;
|
||||
return (
|
||||
<div className="flex justify-end space-x-2 mt-auto pt-6">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSectionGrid({
|
||||
|
||||
41
src/components/Toploader.tsx
Normal file
41
src/components/Toploader.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import * as NProgress from "nprogress";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TopLoader() {
|
||||
return (
|
||||
<>
|
||||
<NextTopLoader showSpinner={false} />
|
||||
<FinishingLoader />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FinishingLoader() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
React.useEffect(() => {
|
||||
NProgress.done();
|
||||
}, [pathname, router, searchParams]);
|
||||
React.useEffect(() => {
|
||||
const linkClickListener = (ev: MouseEvent) => {
|
||||
const element = ev.target as HTMLElement;
|
||||
const closestlink = element.closest("a");
|
||||
const isOpenToNewTabClick =
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey ||
|
||||
ev.metaKey || // apple
|
||||
(ev.button && ev.button == 1); // middle click, >IE9 + everyone else
|
||||
|
||||
if (closestlink && isOpenToNewTabClick) {
|
||||
NProgress.done();
|
||||
}
|
||||
};
|
||||
window.addEventListener("click", linkClickListener);
|
||||
return () => window.removeEventListener("click", linkClickListener);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
@@ -71,8 +71,16 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
disabled={loading || props.disabled} // Disable button when loading
|
||||
{...props}
|
||||
>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{props.children}
|
||||
{asChild ? (
|
||||
props.children
|
||||
) : (
|
||||
<>
|
||||
{loading && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{props.children}
|
||||
</>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
"h-10 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -31,7 +31,8 @@ const toastVariants = cva(
|
||||
variant: {
|
||||
default: "border bg-card text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-white dark:text-destructive-foreground"
|
||||
"destructive group border-destructive bg-destructive text-white dark:text-destructive-foreground",
|
||||
warning: "group border-amber-600 bg-amber-600 text-white"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
12
src/lib/api/getCachedOrg.ts
Normal file
12
src/lib/api/getCachedOrg.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { GetOrgResponse } from "@server/routers/org";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { cache } from "react";
|
||||
import { authCookieHeader } from "./cookies";
|
||||
import { internal } from ".";
|
||||
|
||||
export const getCachedOrg = cache(async (orgId: string) =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
4
src/lib/wait.ts
Normal file
4
src/lib/wait.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function wait(ms: number): Promise<void> {
|
||||
// Wait for the specified amount of time
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
Reference in New Issue
Block a user