diff --git a/messages/en-US.json b/messages/en-US.json index dd2a2d3d8..f716b2562 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1750,6 +1750,26 @@ "authPage": "Auth Page", "authPageDescription": "Configure the auth page for your organization", "authPageDomain": "Auth Page Domain", + "authPageBranding": "Branding", + "authPageBrandingDescription": "Configure the branding for the auth page for your organization", + "authPageBrandingUpdated": "Auth page Branding updated successfully", + "authPageBrandingRemoved": "Auth page Branding removed successfully", + "authPageBrandingRemoveTitle": "Remove Auth Page Branding", + "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", + "authPageBrandingDeleteConfirm": "Confirm Delete Branding", + "brandingLogoURL": "Logo URL", + "brandingPrimaryColor": "Primary Color", + "brandingLogoWidth": "Width (px)", + "brandingLogoHeight": "Height (px)", + "brandingOrgTitle": "Title for Organization Auth Page", + "brandingOrgDescription": "{orgName} will be replaced with the organization's name", + "brandingOrgSubtitle": "Subtitle for Organization Auth Page", + "brandingResourceTitle": "Title for Resource Auth Page", + "brandingResourceSubtitle": "Subtitle for Resource Auth Page", + "brandingResourceDescription": "{resourceName} will be replaced with the organization's name", + "saveAuthPageDomain": "Save Domain", + "saveAuthPageBranding": "Save Branding", + "removeAuthPageBranding": "Remove Branding", "noDomainSet": "No domain set", "changeDomain": "Change Domain", "selectDomain": "Select Domain", @@ -1828,7 +1848,7 @@ "securityPolicyChangeWarningText": "This will affect all users in the organization", "authPageErrorUpdateMessage": "An error occurred while updating the auth page settings", "authPageErrorUpdate": "Unable to update auth page", - "authPageUpdated": "Auth page updated successfully", + "authPageDomainUpdated": "Auth page Domain updated successfully", "healthCheckNotAvailable": "Local", "rewritePath": "Rewrite Path", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", diff --git a/package-lock.json b/package-lock.json index 45ff43219..a3e3c3914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1644,7 +1644,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4074,7 +4073,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -7241,7 +7239,6 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7447,7 +7444,6 @@ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7458,7 +7454,6 @@ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -8893,7 +8888,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -8999,7 +8993,6 @@ "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -9086,7 +9079,6 @@ "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -9180,7 +9172,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -9216,7 +9207,6 @@ "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9250,7 +9240,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -9261,7 +9250,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9405,7 +9393,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -10079,7 +10066,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10609,7 +10595,6 @@ "integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -10722,7 +10707,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -11723,7 +11707,8 @@ "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)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/domutils": { "version": "3.2.2", @@ -12863,7 +12848,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12960,7 +12944,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -13138,7 +13121,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -13447,7 +13429,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -16057,6 +16038,7 @@ "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" @@ -16067,6 +16049,7 @@ "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" }, @@ -16189,7 +16172,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", @@ -18636,7 +18618,6 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -19621,7 +19602,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -19798,7 +19778,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -20256,7 +20235,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -20287,7 +20265,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -21063,7 +21040,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -21557,7 +21533,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22777,8 +22752,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -23792,7 +23766,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24306,7 +24279,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -24613,7 +24585,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 4488d7da6..b1483f97d 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:clear-migrations": "rm -rf server/migrations", - "set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json", - "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json", - "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", + "set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json", + "set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json", + "set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", "set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts", "next:build": "next build", diff --git a/server/db/pg/migrate.ts b/server/db/pg/migrate.ts index 70b2ef549..2d2abca34 100644 --- a/server/db/pg/migrate.ts +++ b/server/db/pg/migrate.ts @@ -10,7 +10,8 @@ const runMigrations = async () => { await migrate(db as any, { migrationsFolder: migrationsFolder }); - console.log("Migrations completed successfully."); + console.log("Migrations completed successfully. ✅"); + process.exit(0); } catch (error) { console.error("Error running migrations:", error); process.exit(1); diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 17d262c61..1f30dbf5d 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -204,6 +204,29 @@ export const loginPageOrg = pgTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = pgTable("loginPageBranding", { + loginPageBrandingId: serial("loginPageBrandingId").primaryKey(), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = pgTable("sessionTransferToken", { token: varchar("token").primaryKey(), sessionId: varchar("sessionId") @@ -215,42 +238,56 @@ export const sessionTransferToken = pgTable("sessionTransferToken", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() }); -export const actionAuditLog = pgTable("actionAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }).notNull(), - actor: varchar("actor", { length: 255 }).notNull(), - actorId: varchar("actorId", { length: 255 }).notNull(), - action: varchar("action", { length: 100 }).notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = pgTable( + "actionAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }).notNull(), + actor: varchar("actor", { length: 255 }).notNull(), + actorId: varchar("actorId", { length: 255 }).notNull(), + action: varchar("action", { length: 100 }).notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = pgTable("accessAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }), - actor: varchar("actor", { length: 255 }), - actorId: varchar("actorId", { length: 255 }), - resourceId: integer("resourceId"), - ip: varchar("ip", { length: 45 }), - type: varchar("type", { length: 100 }).notNull(), - action: boolean("action").notNull(), - location: text("location"), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = pgTable( + "accessAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }), + actor: varchar("actor", { length: 255 }), + actorId: varchar("actorId", { length: 255 }), + resourceId: integer("resourceId"), + ip: varchar("ip", { length: 45 }), + type: varchar("type", { length: 100 }).notNull(), + action: boolean("action").notNull(), + location: text("location"), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -269,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ffbe820cc..0b750d4ee 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -7,7 +7,8 @@ import { bigint, real, text, - index + index, + uniqueIndex } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { randomUUID } from "crypto"; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 653967700..930566659 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -29,7 +29,9 @@ export const certificates = sqliteTable("certificates", { }); export const dnsChallenge = sqliteTable("dnsChallenges", { - dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }), + dnsChallengeId: integer("dnsChallengeId").primaryKey({ + autoIncrement: true + }), domain: text("domain").notNull(), token: text("token").notNull(), keyAuthorization: text("keyAuthorization").notNull(), @@ -61,9 +63,7 @@ export const customers = sqliteTable("customers", { }); export const subscriptions = sqliteTable("subscriptions", { - subscriptionId: text("subscriptionId") - .primaryKey() - .notNull(), + subscriptionId: text("subscriptionId").primaryKey().notNull(), customerId: text("customerId") .notNull() .references(() => customers.customerId, { onDelete: "cascade" }), @@ -75,7 +75,9 @@ export const subscriptions = sqliteTable("subscriptions", { }); export const subscriptionItems = sqliteTable("subscriptionItems", { - subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }), + subscriptionItemId: integer("subscriptionItemId").primaryKey({ + autoIncrement: true + }), subscriptionId: text("subscriptionId") .notNull() .references(() => subscriptions.subscriptionId, { @@ -129,7 +131,9 @@ export const limits = sqliteTable("limits", { }); export const usageNotifications = sqliteTable("usageNotifications", { - notificationId: integer("notificationId").primaryKey({ autoIncrement: true }), + notificationId: integer("notificationId").primaryKey({ + autoIncrement: true + }), orgId: text("orgId") .notNull() .references(() => orgs.orgId, { onDelete: "cascade" }), @@ -199,6 +203,31 @@ export const loginPageOrg = sqliteTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = sqliteTable("loginPageBranding", { + loginPageBrandingId: integer("loginPageBrandingId").primaryKey({ + autoIncrement: true + }), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = sqliteTable("sessionTransferToken", { token: text("token").primaryKey(), sessionId: text("sessionId") @@ -210,42 +239,56 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", { expiresAt: integer("expiresAt").notNull() }); -export const actionAuditLog = sqliteTable("actionAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType").notNull(), - actor: text("actor").notNull(), - actorId: text("actorId").notNull(), - action: text("action").notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = sqliteTable( + "actionAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType").notNull(), + actor: text("actor").notNull(), + actorId: text("actorId").notNull(), + action: text("action").notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = sqliteTable("accessAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType"), - actor: text("actor"), - actorId: text("actorId"), - resourceId: integer("resourceId"), - ip: text("ip"), - location: text("location"), - type: text("type").notNull(), - action: integer("action", { mode: "boolean" }).notNull(), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = sqliteTable( + "accessAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType"), + actor: text("actor"), + actorId: text("actorId"), + resourceId: integer("resourceId"), + ip: text("ip"), + location: text("location"), + type: text("type").notNull(), + action: integer("action", { mode: "boolean" }).notNull(), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -264,5 +307,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 13453d2e4..c96fefc5c 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,6 +1,12 @@ import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + index, + uniqueIndex +} from "drizzle-orm/sqlite-core"; import { boolean } from "yargs"; export const domains = sqliteTable("domains", { @@ -25,11 +31,10 @@ export const dnsRecords = sqliteTable("dnsRecords", { recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" baseDomain: text("baseDomain"), - value: text("value").notNull(), - verified: integer("verified", { mode: "boolean" }).notNull().default(false), + value: text("value").notNull(), + verified: integer("verified", { mode: "boolean" }).notNull().default(false) }); - export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), @@ -142,9 +147,10 @@ export const resources = sqliteTable("resources", { onDelete: "set null" }), headers: text("headers"), // comma-separated list of headers to add to the request - proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false), + proxyProtocol: integer("proxyProtocol", { mode: "boolean" }) + .notNull() + .default(false), proxyProtocolVersion: integer("proxyProtocolVersion").default(1) - }); export const targets = sqliteTable("targets", { diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index eefd175cd..5fe8d538b 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -312,6 +312,33 @@ authenticated.get( loginPage.getLoginPage ); +authenticated.get( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getLoginPage), + logActionAudit(ActionsEnum.getLoginPage), + loginPage.getLoginPageBranding +); + +authenticated.put( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateLoginPage), + logActionAudit(ActionsEnum.updateLoginPage), + loginPage.upsertLoginPageBranding +); + +authenticated.delete( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteLoginPage), + logActionAudit(ActionsEnum.deleteLoginPage), + loginPage.deleteLoginPageBranding +); + authRouter.post( "/remoteExitNode/get-token", verifyValidLicense, diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index b393b8843..49596a1f9 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -28,6 +28,7 @@ internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); internalRouter.get("/login-page", loginPage.loadLoginPage); +internalRouter.get("/login-page-branding", loginPage.loadLoginPageBranding); internalRouter.post( "/get-session-transfer-token", diff --git a/server/private/routers/loginPage/deleteLoginPageBranding.ts b/server/private/routers/loginPage/deleteLoginPageBranding.ts new file mode 100644 index 000000000..1fb243b04 --- /dev/null +++ b/server/private/routers/loginPage/deleteLoginPageBranding.ts @@ -0,0 +1,113 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function deleteLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + await db + .delete(loginPageBranding) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ); + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts new file mode 100644 index 000000000..262e9ce82 --- /dev/null +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -0,0 +1,103 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function getLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/index.ts b/server/private/routers/loginPage/index.ts index 2372ddfa9..1bfe6e16c 100644 --- a/server/private/routers/loginPage/index.ts +++ b/server/private/routers/loginPage/index.ts @@ -17,3 +17,7 @@ export * from "./getLoginPage"; export * from "./loadLoginPage"; export * from "./updateLoginPage"; export * from "./deleteLoginPage"; +export * from "./upsertLoginPageBranding"; +export * from "./deleteLoginPageBranding"; +export * from "./getLoginPageBranding"; +export * from "./loadLoginPageBranding"; diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts new file mode 100644 index 000000000..823f75a6a --- /dev/null +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -0,0 +1,100 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, loginPageBranding, loginPageBrandingOrg, 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 { fromError } from "zod-validation-error"; +import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; + +const querySchema = z.object({ + orgId: z.string().min(1) +}); + +async function query(orgId: string) { + const [orgLink] = await db + .select() + .from(loginPageBrandingOrg) + .where(eq(loginPageBrandingOrg.orgId, orgId)) + .innerJoin(orgs, eq(loginPageBrandingOrg.orgId, orgs.orgId)); + if (!orgLink) { + return null; + } + + const [res] = await db + .select() + .from(loginPageBranding) + .where( + and( + eq( + loginPageBranding.loginPageBrandingId, + orgLink.loginPageBrandingOrg.loginPageBrandingId + ) + ) + ) + .limit(1); + return { + ...res, + orgId: orgLink.orgs.orgId, + orgName: orgLink.orgs.name + }; +} + +export async function loadLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedQuery.data; + + const branding = await query(orgId); + + if (!branding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Branding for Login page not found" + ) + ); + } + + return response(res, { + data: branding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts new file mode 100644 index 000000000..f9f9d08c1 --- /dev/null +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -0,0 +1,162 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq, InferInsertModel } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z.strictObject({ + orgId: z.string() +}); + +const bodySchema = z.strictObject({ + logoUrl: z.url(), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); + +export type UpdateLoginPageBrandingBody = z.infer; + +export async function upsertLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + let updateData = parsedBody.data satisfies InferInsertModel< + typeof loginPageBranding + >; + + if (build !== "saas") { + // org branding settings are only considered in the saas build + const { orgTitle, orgSubtitle, ...rest } = updateData; + updateData = rest; + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + let updatedLoginPageBranding: LoginPageBranding; + + if (existingLoginPageBranding) { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .update(loginPageBranding) + .set({ ...updateData }) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ) + .returning(); + return branding; + }); + } else { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .insert(loginPageBranding) + .values({ ...updateData }) + .returning(); + + await tx.insert(loginPageBrandingOrg).values({ + loginPageBrandingId: branding.loginPageBrandingId, + orgId: orgId + }); + return branding; + }); + } + + return response(res, { + data: updatedLoginPageBranding, + success: true, + error: false, + message: existingLoginPageBranding + ? "Login page branding updated successfully" + : "Login page branding created successfully", + status: existingLoginPageBranding ? HttpCode.OK : HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index f500f483a..e8ecdbaa2 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -80,7 +80,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateOrg), logActionAudit(ActionsEnum.updateOrg), - org.updateOrg, + org.updateOrg ); if (build !== "saas") { @@ -90,7 +90,7 @@ if (build !== "saas") { verifyUserIsOrgOwner, verifyUserHasAction(ActionsEnum.deleteOrg), logActionAudit(ActionsEnum.deleteOrg), - org.deleteOrg, + org.deleteOrg ); } @@ -157,7 +157,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), logActionAudit(ActionsEnum.createClient), - client.createClient, + client.createClient ); authenticated.delete( @@ -166,7 +166,7 @@ authenticated.delete( verifyClientAccess, verifyUserHasAction(ActionsEnum.deleteClient), logActionAudit(ActionsEnum.deleteClient), - client.deleteClient, + client.deleteClient ); authenticated.post( @@ -175,7 +175,7 @@ authenticated.post( verifyClientAccess, // this will check if the user has access to the client verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client logActionAudit(ActionsEnum.updateClient), - client.updateClient, + client.updateClient ); @@ -190,7 +190,7 @@ authenticated.post( verifySiteAccess, verifyUserHasAction(ActionsEnum.updateSite), logActionAudit(ActionsEnum.updateSite), - site.updateSite, + site.updateSite ); authenticated.delete( @@ -198,7 +198,7 @@ authenticated.delete( verifySiteAccess, verifyUserHasAction(ActionsEnum.deleteSite), logActionAudit(ActionsEnum.deleteSite), - site.deleteSite, + site.deleteSite ); // TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite" @@ -218,13 +218,13 @@ authenticated.post( "/site/:siteId/docker/check", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.checkDockerSocket, + site.checkDockerSocket ); authenticated.post( "/site/:siteId/docker/trigger", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.triggerFetchContainers, + site.triggerFetchContainers ); authenticated.get( "/site/:siteId/docker/containers", @@ -240,7 +240,7 @@ authenticated.put( verifySiteAccess, verifyUserHasAction(ActionsEnum.createSiteResource), logActionAudit(ActionsEnum.createSiteResource), - siteResource.createSiteResource, + siteResource.createSiteResource ); authenticated.get( @@ -274,7 +274,7 @@ authenticated.post( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.updateSiteResource), logActionAudit(ActionsEnum.updateSiteResource), - siteResource.updateSiteResource, + siteResource.updateSiteResource ); authenticated.delete( @@ -284,7 +284,7 @@ authenticated.delete( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource), - siteResource.deleteSiteResource, + siteResource.deleteSiteResource ); authenticated.put( @@ -292,7 +292,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), logActionAudit(ActionsEnum.createResource), - resource.createResource, + resource.createResource ); authenticated.get( @@ -354,7 +354,7 @@ authenticated.delete( verifyOrgAccess, verifyUserHasAction(ActionsEnum.removeInvitation), logActionAudit(ActionsEnum.removeInvitation), - user.removeInvitation, + user.removeInvitation ); authenticated.post( @@ -362,7 +362,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.inviteUser), logActionAudit(ActionsEnum.inviteUser), - user.inviteUser, + user.inviteUser ); // maybe make this /invite/create instead unauthenticated.post("/invite/accept", user.acceptInvite); // this is supposed to be unauthenticated @@ -398,14 +398,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResource), logActionAudit(ActionsEnum.updateResource), - resource.updateResource, + resource.updateResource ); authenticated.delete( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResource), logActionAudit(ActionsEnum.deleteResource), - resource.deleteResource, + resource.deleteResource ); authenticated.put( @@ -413,7 +413,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createTarget), logActionAudit(ActionsEnum.createTarget), - target.createTarget, + target.createTarget ); authenticated.get( "/resource/:resourceId/targets", @@ -427,7 +427,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createResourceRule), logActionAudit(ActionsEnum.createResourceRule), - resource.createResourceRule, + resource.createResourceRule ); authenticated.get( "/resource/:resourceId/rules", @@ -440,14 +440,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResourceRule), logActionAudit(ActionsEnum.updateResourceRule), - resource.updateResourceRule, + resource.updateResourceRule ); authenticated.delete( "/resource/:resourceId/rule/:ruleId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResourceRule), logActionAudit(ActionsEnum.deleteResourceRule), - resource.deleteResourceRule, + resource.deleteResourceRule ); authenticated.get( @@ -461,14 +461,14 @@ authenticated.post( verifyTargetAccess, verifyUserHasAction(ActionsEnum.updateTarget), logActionAudit(ActionsEnum.updateTarget), - target.updateTarget, + target.updateTarget ); authenticated.delete( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.deleteTarget), logActionAudit(ActionsEnum.deleteTarget), - target.deleteTarget, + target.deleteTarget ); authenticated.put( @@ -476,7 +476,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createRole), logActionAudit(ActionsEnum.createRole), - role.createRole, + role.createRole ); authenticated.get( "/org/:orgId/roles", @@ -502,7 +502,7 @@ authenticated.delete( verifyRoleAccess, verifyUserHasAction(ActionsEnum.deleteRole), logActionAudit(ActionsEnum.deleteRole), - role.deleteRole, + role.deleteRole ); authenticated.post( "/role/:roleId/add/:userId", @@ -510,7 +510,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.addUserRole), logActionAudit(ActionsEnum.addUserRole), - user.addUserRole, + user.addUserRole ); authenticated.post( @@ -519,7 +519,7 @@ authenticated.post( verifyRoleAccess, verifyUserHasAction(ActionsEnum.setResourceRoles), logActionAudit(ActionsEnum.setResourceRoles), - resource.setResourceRoles, + resource.setResourceRoles ); authenticated.post( @@ -528,7 +528,7 @@ authenticated.post( verifySetResourceUsers, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - resource.setResourceUsers, + resource.setResourceUsers ); authenticated.post( @@ -536,7 +536,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePassword), logActionAudit(ActionsEnum.setResourcePassword), - resource.setResourcePassword, + resource.setResourcePassword ); authenticated.post( @@ -544,7 +544,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePincode), logActionAudit(ActionsEnum.setResourcePincode), - resource.setResourcePincode, + resource.setResourcePincode ); authenticated.post( @@ -552,7 +552,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), logActionAudit(ActionsEnum.setResourceHeaderAuth), - resource.setResourceHeaderAuth, + resource.setResourceHeaderAuth ); authenticated.post( @@ -560,7 +560,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceWhitelist), logActionAudit(ActionsEnum.setResourceWhitelist), - resource.setResourceWhitelist, + resource.setResourceWhitelist ); authenticated.get( @@ -575,7 +575,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.generateAccessToken), logActionAudit(ActionsEnum.generateAccessToken), - accessToken.generateAccessToken, + accessToken.generateAccessToken ); authenticated.delete( @@ -583,7 +583,7 @@ authenticated.delete( verifyAccessTokenAccess, verifyUserHasAction(ActionsEnum.deleteAcessToken), logActionAudit(ActionsEnum.deleteAcessToken), - accessToken.deleteAccessToken, + accessToken.deleteAccessToken ); authenticated.get( @@ -657,7 +657,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgUser), logActionAudit(ActionsEnum.createOrgUser), - user.createOrgUser, + user.createOrgUser ); authenticated.post( @@ -666,7 +666,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.updateOrgUser), logActionAudit(ActionsEnum.updateOrgUser), - user.updateOrgUser, + user.updateOrgUser ); authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); @@ -690,7 +690,7 @@ authenticated.delete( verifyUserAccess, verifyUserHasAction(ActionsEnum.removeUser), logActionAudit(ActionsEnum.removeUser), - user.removeUserOrg, + user.removeUserOrg ); // authenticated.put( @@ -821,7 +821,7 @@ authenticated.post( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.setApiKeyActions), logActionAudit(ActionsEnum.setApiKeyActions), - apiKeys.setApiKeyActions, + apiKeys.setApiKeyActions ); authenticated.get( @@ -837,7 +837,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createApiKey), logActionAudit(ActionsEnum.createApiKey), - apiKeys.createOrgApiKey, + apiKeys.createOrgApiKey ); authenticated.delete( @@ -846,7 +846,7 @@ authenticated.delete( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.deleteApiKey), logActionAudit(ActionsEnum.deleteApiKey), - apiKeys.deleteOrgApiKey, + apiKeys.deleteOrgApiKey ); authenticated.get( @@ -862,7 +862,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgDomain), logActionAudit(ActionsEnum.createOrgDomain), - domain.createOrgDomain, + domain.createOrgDomain ); authenticated.post( @@ -871,7 +871,7 @@ authenticated.post( verifyDomainAccess, verifyUserHasAction(ActionsEnum.restartOrgDomain), logActionAudit(ActionsEnum.restartOrgDomain), - domain.restartOrgDomain, + domain.restartOrgDomain ); authenticated.delete( @@ -880,7 +880,7 @@ authenticated.delete( verifyDomainAccess, verifyUserHasAction(ActionsEnum.deleteOrgDomain), logActionAudit(ActionsEnum.deleteOrgDomain), - domain.deleteAccountDomain, + domain.deleteAccountDomain ); authenticated.get( @@ -1239,4 +1239,4 @@ authRouter.delete( store: createStore() }), auth.deleteSecurityKey -); \ No newline at end of file +); diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index 26f59cab1..8a253d072 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -1,4 +1,4 @@ -import { LoginPage } from "@server/db"; +import type { LoginPage, LoginPageBranding } from "@server/db"; export type CreateLoginPageResponse = LoginPage; @@ -8,4 +8,11 @@ export type GetLoginPageResponse = LoginPage; export type UpdateLoginPageResponse = LoginPage; -export type LoadLoginPageResponse = LoginPage & { orgId: string }; \ No newline at end of file +export type LoadLoginPageResponse = LoginPage & { orgId: string }; + +export type LoadLoginPageBrandingResponse = LoginPageBranding & { + orgId: string; + orgName: string; +}; + +export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 60f8e5862..75672cac7 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -89,7 +89,6 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - .leftJoin( resourceHeaderAuth, eq( diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 538c7fde6..c4048bcc8 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,11 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; type BillingSettingsProps = { children: React.ReactNode; @@ -19,12 +14,11 @@ type BillingSettingsProps = { export default async function BillingSettingsPage({ children, - params, + params }: BillingSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +26,7 @@ export default async function BillingSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +34,7 @@ export default async function BillingSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader(), - ), - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -65,11 +47,11 @@ export default async function BillingSettingsPage({ - {children} + {children} diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx index 7cdea07a5..6cdbf23c0 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse as GetOrgIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/idp`); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { title: t("general"), href: `/${params.orgId}/settings/idp/${params.idpId}/general` diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx new file mode 100644 index 000000000..139449bf0 --- /dev/null +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -0,0 +1,68 @@ +import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm"; +import AuthPageSettings from "@app/components/private/AuthPageSettings"; +import { SettingsContainer } from "@app/components/Settings"; +import { internal, priv } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { pullEnv } from "@app/lib/pullEnv"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { + GetLoginPageBrandingResponse, + GetLoginPageResponse +} from "@server/routers/loginPage/types"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; + +export interface AuthPageProps { + params: Promise<{ orgId: string }>; +} + +export default async function AuthPage(props: AuthPageProps) { + const orgId = (await props.params).orgId; + const env = pullEnv(); + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (!subscribed) { + redirect(env.app.dashboardUrl); + } + + let loginPage: GetLoginPageResponse | null = null; + try { + if (build === "saas") { + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() + ); + if (res.status === 200) { + loginPage = res.data.data; + } + } + } catch (error) {} + + let loginPageBranding: GetLoginPageBrandingResponse | null = null; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${orgId}/login-page-branding`, await authCookieHeader()); + if (res.status === 200) { + loginPageBranding = res.data.data; + } + } catch (error) {} + + return ( + + {build === "saas" && } + + + ); +} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 82b2c9991..812b94918 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -1,16 +1,15 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; + import { redirect } from "next/navigation"; -import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; + +import { getTranslations } from "next-intl/server"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { build } from "@server/build"; type GeneralSettingsProps = { children: React.ReactNode; @@ -19,12 +18,11 @@ type GeneralSettingsProps = { export default async function GeneralSettingsPage({ children, - params, + params }: GeneralSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +30,7 @@ export default async function GeneralSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +38,7 @@ export default async function GeneralSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader(), - ), - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -60,25 +46,30 @@ export default async function GeneralSettingsPage({ const t = await getTranslations(); - const navItems = [ + const navItems: TabItem[] = [ { - title: t('general'), + title: t("general"), href: `/{orgId}/settings/general`, - }, + exact: true + } ]; + if (build === "saas") { + navItems.push({ + title: t("authPage"), + href: `/{orgId}/settings/general/auth-page` + }); + } return ( <> - - {children} - + {children} diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index fdedba5c7..54d221884 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -43,16 +43,16 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter + SettingsSectionForm } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { SwitchInput } from "@app/components/SwitchInput"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; // Session length options in hours const SESSION_LENGTH_OPTIONS = [ @@ -102,34 +102,23 @@ const LOG_RETENTION_OPTIONS = [ { label: "logRetention14Days", value: 14 }, { label: "logRetention30Days", value: 30 }, { label: "logRetention90Days", value: 90 }, - ...(build != "saas" ? [{ label: "logRetentionForever", value: -1 }] : []) + ...(build !== "saas" ? [{ label: "logRetentionForever", value: -1 }] : []) ]; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { orgUser } = userOrgUserContext(); const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); const { user } = useUserContext(); const t = useTranslations(); const { env } = useEnvContext(); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); - const subscription = useSubscriptionStatusContext(); - - // Check if security features are disabled due to licensing/subscription - const isSecurityFeatureDisabled = () => { - const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); - const isSaasNotSubscribed = - build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed; - }; + const { isPaidUser, hasSaasSubscription } = usePaidStatus(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = useState(false); - const authPageSettingsRef = useRef(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), @@ -252,14 +241,6 @@ export default function GeneralPage() { // Update organization await api.post(`/org/${org?.org.orgId}`, reqData); - // Also save auth page settings if they have unsaved changes - if ( - build === "saas" && - authPageSettingsRef.current?.hasUnsavedChanges() - ) { - await authPageSettingsRef.current.saveAuthSettings(); - } - toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") @@ -408,9 +389,7 @@ export default function GeneralPage() { {LOG_RETENTION_OPTIONS.filter( (option) => { if ( - build == - "saas" && - !subscription?.subscribed && + hasSaasSubscription && option.value > 30 ) { @@ -438,19 +417,15 @@ export default function GeneralPage() { )} /> - {build != "oss" && ( + {build !== "oss" && ( <> - + { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -516,11 +491,7 @@ export default function GeneralPage() { control={form.control} name="settingsLogRetentionDaysAction" render={({ field }) => { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -600,13 +571,12 @@ export default function GeneralPage() { - + { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -653,8 +623,7 @@ export default function GeneralPage() { control={form.control} name="maxSessionLengthHours" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -740,8 +709,7 @@ export default function GeneralPage() { control={form.control} name="passwordExpiryDays" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -832,8 +800,6 @@ export default function GeneralPage() { - {build === "saas" && } -
{build !== "saas" && ( + + + + + + +
+ {branding && ( + + )} + +
+ + + ); +} diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index 540b8e0e2..139d76b43 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; type BrandingLogoProps = { + logoPath?: string; width: number; height: number; }; @@ -38,16 +39,19 @@ export default function BrandingLogo(props: BrandingLogoProps) { if (isUnlocked() && env.branding.logo?.darkPath) { return env.branding.logo.darkPath; } - return "/logo/word_mark_white.png"; + return "/logo/word_mark_white.png"; } - const path = getPath(); - setPath(path); - }, [theme, env]); + setPath(props.logoPath ?? getPath()); + }, [theme, env, props.logoPath]); + + // we use `img` tag here because the `logoPath` could be any URL + // and next.js `Image` component only accepts a restricted number of domains + const Component = props.logoPath ? "img" : Image; return ( path && ( - Logo) { - setLoading(true); + async function onSubmit() { try { await onConfirm(); setOpen(false); - reset(); + form.reset(); } catch (error) { // Handle error if needed console.error("Confirmation failed:", error); - } finally { - setLoading(false); } } @@ -110,7 +80,7 @@ export default function InviteUserForm({ open={open} onOpenChange={(val) => { setOpen(val); - reset(); + form.reset(); }} > @@ -136,7 +106,7 @@ export default function InviteUserForm({
diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 24f510dcd..50a83611c 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -521,13 +521,13 @@ export default function DomainPicker2({
{selectedBaseDomain.type === "organization" ? null : ( - + )} {selectedBaseDomain.domain} {selectedBaseDomain.verified && ( - + )}
) : ( @@ -747,7 +747,11 @@ export default function DomainPicker2({ handleProvidedDomainSelect(option); } }} - className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} + style={{ + // @ts-expect-error CSS variable + "--cols": `repeat(${cols}, minmax(0, 1fr))` + }} + className="grid gap-2 grid-cols-1 sm:grid-cols-(--cols)" > {displayedProvidedOptions.map((option) => (