diff --git a/cli/commands/setServerAdmin.ts b/cli/commands/setServerAdmin.ts index 341b70bc1..0cab8cc9f 100644 --- a/cli/commands/setServerAdmin.ts +++ b/cli/commands/setServerAdmin.ts @@ -4,19 +4,26 @@ import { eq } from "drizzle-orm"; type SetServerAdminArgs = { email: string; + remove: boolean; }; export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = { command: "set-server-admin", - describe: "Mark any user as a server admin by email address", + describe: "Add or remove server admin by email address", builder: (yargs) => { - return yargs.option("email", { - type: "string", - demandOption: true, - describe: "User email address" - }); + return yargs + .option("email", { + type: "string", + demandOption: true, + describe: "User email address" + }) + .option("remove", { + type: "boolean", + default: false, + describe: "Remove server admin status from the user" + }); }, - handler: async (argv: { email: string }) => { + handler: async (argv: SetServerAdminArgs) => { try { const email = argv.email.trim().toLowerCase(); @@ -31,6 +38,33 @@ export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = { process.exit(1); } + if (argv.remove) { + if (!user.serverAdmin) { + console.log(`User '${email}' is not a server admin`); + process.exit(0); + } + + const serverAdmins = await db + .select() + .from(users) + .where(eq(users.serverAdmin, true)); + + if (serverAdmins.length <= 1) { + console.error( + "Cannot remove server admin: at least one server admin must exist" + ); + process.exit(1); + } + + await db + .update(users) + .set({ serverAdmin: false }) + .where(eq(users.userId, user.userId)); + + console.log(`Server admin status removed from user '${email}'`); + process.exit(0); + } + if (user.serverAdmin) { console.log(`User '${email}' is already a server admin`); process.exit(0); diff --git a/messages/en-US.json b/messages/en-US.json index f389374c1..e490e82ef 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -214,6 +214,7 @@ "resourceErrorDelte": "Error deleting resource", "resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules", "resourcePoliciesBannerDescription": "Shared resource policies let you define authentication methods and access rules once, then attach them to multiple public resources. When you update a policy, every linked resource inherits the change automatically.", + "resourcePoliciesBannerButtonText": "Learn More", "resourcePoliciesTitle": "Manage Public Resource Policies", "resourcePoliciesAttachedResourcesColumnTitle": "Resources", "resourcePoliciesAttachedResources": "{count} resource(s)", @@ -280,7 +281,7 @@ "back": "Back", "cancel": "Cancel", "resourceConfig": "Configuration Snippets", - "resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource", + "resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource.", "resourceAddEntrypoints": "Traefik: Add Entrypoints", "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", "resourceLearnRaw": "Learn how to configure TCP/UDP resources", @@ -727,7 +728,7 @@ "targetSubmit": "Add Target", "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.", - "targetsSubmit": "Save Targets", + "targetsSubmit": "Save Settings", "addTarget": "Add Target", "proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.", "targetErrorInvalidIp": "Invalid IP address", @@ -982,7 +983,9 @@ "resourcePolicySharedDescription": "This resource uses a shared policy.", "sharedPolicy": "Shared Policy", "sharedPolicyNoneDescription": "This resource has its own policy.", - "resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource. To change the underlying policy, you must edit to {policyName}.", + "resourceSharedPolicyOwnDescription": "This resource has its own authentication and access rules controls.", + "resourceSharedPolicyInheritedDescription": "This resource inherits authentication and access rules controls from {policyName}.", + "resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource to add to the policy. To change the underlying policy, you must edit to {policyName}.", "resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit {policyName}.", "resourceUsersRoles": "Access Controls", "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", @@ -1008,7 +1011,14 @@ "resourceVisibilityTitle": "Visibility", "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", "resourceGeneral": "General Settings", - "resourceGeneralDescription": "Configure the general settings for this resource", + "resourceGeneralDescription": "Configure name, address, and access policy for this resource.", + "resourceGeneralDetailsSubsection": "Resource Details", + "resourceGeneralDetailsSubsectionDescription": "Set the display name, identifier, and publicly accessible domain for this resource.", + "resourceGeneralDetailsSubsectionPortDescription": "Set the display name, identifier, and public port for this resource.", + "resourceGeneralPublicAddressSubsection": "Public Address", + "resourceGeneralPublicAddressSubsectionDescription": "Configure how users reach this resource.", + "resourceGeneralAuthenticationAccessSubsection": "Authentication & Access", + "resourceGeneralAuthenticationAccessSubsectionDescription": "Choose whether this resource uses its own policy or inherits from a shared policy.", "resourceEnable": "Enable Resource", "resourceTransfer": "Transfer Resource", "resourceTransferDescription": "Transfer this resource to a different site", @@ -1734,10 +1744,10 @@ "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to the site connector. Read about how this works in the documentation.", "newtAutoUpdate": "Enable Site Auto-Update", - "newtAutoUpdateDescription": "When enabled, site connectors will automatically update to the latest version when a new release is available.", + "newtAutoUpdateDescription": "When enabled, site connectors will automatically download the latest version and restart themselves. This can be overridden on a per-site basis.", "siteAutoUpdate": "Site Auto-Update", "siteAutoUpdateLabel": "Enable Auto-Update", - "siteAutoUpdateDescription": "Control whether this site's connector automatically downloads the latest version.", + "siteAutoUpdateDescription": "When enabled, this site's connector will automatically download the latest version and restart itself.", "siteAutoUpdateOrgDefault": "Organization default: {state}", "siteAutoUpdateOverriding": "Overriding organization setting", "siteAutoUpdateResetToOrg": "Reset to Organization Default", @@ -1835,9 +1845,9 @@ "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", "documentation": "Documentation", "saveAllSettings": "Save All Settings", - "saveResourceTargets": "Save Targets", - "saveResourceHttp": "Save Proxy Settings", - "saveProxyProtocol": "Save Proxy protocol settings", + "saveResourceTargets": "Save Settings", + "saveResourceHttp": "Save Settings", + "saveProxyProtocol": "Save Settings", "settingsUpdated": "Settings updated", "settingsUpdatedDescription": "Settings updated successfully", "settingsErrorUpdate": "Failed to update settings", @@ -2964,9 +2974,10 @@ "enableProxyProtocol": "Enable Proxy Protocol", "proxyProtocolInfo": "Preserve client IP addresses for TCP backends", "proxyProtocolVersion": "Proxy Protocol Version", - "version1": " Version 1 (Recommended)", + "version1": "Version 1 (Recommended)", "version2": "Version 2", - "versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.", + "version1Description": "Text-based and widely supported. Make sure servers transport is added to dynamic config.", + "version2Description": "Binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.", "warning": "Warning", "proxyProtocolWarning": "The backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections so only enable this if you know what you're doing. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.", "restarting": "Restarting...", @@ -3172,6 +3183,8 @@ "warning:": "Warning:", "forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.", "pageTitle": "Page Title", + "maintenancePageContentSubsection": "Page Content", + "maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page", "pageTitleDescription": "The main heading displayed on the maintenance page", "maintenancePageMessage": "Maintenance Message", "maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.", @@ -3531,14 +3544,14 @@ "sshConnecting": "Connecting…", "sshInitializing": "Initializing…", "sshSignInTitle": "Sign in to SSH", - "sshSignInDescription": "Enter your SSH credentials", + "sshSignInDescription": "Enter your SSH credentials to connect", "sshPasswordTab": "Password", "sshPrivateKeyTab": "Private Key", "sshPrivateKeyField": "Private Key", "sshPrivateKeyDisclaimer": "Your private key is not stored or visible to Pangolin. Alternatively, you can use short-lived certificates for seamless authentication using your existing Pangolin identity.", "sshLearnMore": "Learn more", "sshPrivateKeyFile": "Private Key File", - "sshAuthenticate": "Authenticate", + "sshAuthenticate": "Connect", "sshTerminate": "Terminate", "sshPoweredBy": "Powered by", "sshErrorNoTarget": "No target specified", diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9f6901dda..9e07a6234 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -87,7 +87,7 @@ function createDb() { export const db = createDb(); export default db; -export const primaryDb = db.$primary as typeof db; // is this typeof a problem - techincally they are different types +export const primaryDb = db.$primary as typeof db; // is this typeof a problem - technically they are different types export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; diff --git a/server/db/pg/logsDriver.ts b/server/db/pg/logsDriver.ts index 146b8fb2f..2c34136de 100644 --- a/server/db/pg/logsDriver.ts +++ b/server/db/pg/logsDriver.ts @@ -2,7 +2,7 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { readConfigFile } from "@server/lib/readConfigFile"; import { withReplicas } from "drizzle-orm/pg-core"; import { build } from "@server/build"; -import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver"; +import { db as mainDb } from "./driver"; import { createPool } from "./poolConfig"; function createLogsDb() { @@ -63,8 +63,7 @@ function createLogsDb() { }) ); } else { - const maxReplicaConnections = - poolConfig?.max_replica_connections || 20; + const maxReplicaConnections = poolConfig?.max_replica_connections || 20; for (const conn of replicaConnections) { const replicaPool = createPool( conn.connection_string, @@ -91,4 +90,4 @@ function createLogsDb() { export const logsDb = createLogsDb(); export default logsDb; -export const primaryLogsDb = logsDb.$primary; \ No newline at end of file +export const primaryLogsDb = logsDb.$primary; diff --git a/server/db/pg/poolConfig.ts b/server/db/pg/poolConfig.ts index f753121c1..b893c2159 100644 --- a/server/db/pg/poolConfig.ts +++ b/server/db/pg/poolConfig.ts @@ -1,5 +1,4 @@ import { Pool, PoolConfig } from "pg"; -import logger from "@server/logger"; export function createPoolConfig( connectionString: string, @@ -27,7 +26,7 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void { pool.on("error", (err) => { // This catches errors on idle clients in the pool. Without this // handler an unexpected disconnect would crash the process. - logger.error( + console.error( `Unexpected error on idle ${label} database client: ${err.message}` ); }); @@ -36,7 +35,7 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void { // Set a statement timeout on every new connection so a single slow // query can't block the pool forever client.query("SET statement_timeout = '30s'").catch((err: Error) => { - logger.warn( + console.warn( `Failed to set statement_timeout on ${label} client: ${err.message}` ); }); @@ -60,4 +59,4 @@ export function createPool( ); attachPoolErrorHandlers(pool, label); return pool; -} \ No newline at end of file +} diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index b7b34a5d7..025bdf923 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -147,12 +147,10 @@ export const resources = pgTable("resources", { }), ssl: boolean("ssl").notNull().default(false), blockAccess: boolean("blockAccess").notNull().default(false), - sso: boolean("sso").notNull().default(true), proxyPort: integer("proxyPort"), - emailWhitelistEnabled: boolean("emailWhitelistEnabled") - .notNull() - .default(false), - applyRules: boolean("applyRules").notNull().default(false), + sso: boolean("sso"), + emailWhitelistEnabled: boolean("emailWhitelistEnabled"), + applyRules: boolean("applyRules"), enabled: boolean("enabled").notNull().default(true), stickySession: boolean("stickySession").notNull().default(false), tlsServerName: varchar("tlsServerName"), diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 302671b0d..0a22e8df3 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -45,9 +45,9 @@ export type ResourceWithAuth = { password: ResourcePassword | ResourcePolicyPassword | null; headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; - applyRules: boolean; - sso: boolean; - emailWhitelistEnabled: boolean; + applyRules: boolean | null; + sso: boolean | null; + emailWhitelistEnabled: boolean | null; org: Org; }; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 639e3cf4f..0c4a143f5 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -165,14 +165,12 @@ export const resources = sqliteTable("resources", { blockAccess: integer("blockAccess", { mode: "boolean" }) .notNull() .default(false), - sso: integer("sso", { mode: "boolean" }).notNull().default(true), proxyPort: integer("proxyPort"), - emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) - .notNull() - .default(false), - applyRules: integer("applyRules", { mode: "boolean" }) - .notNull() - .default(false), + sso: integer("sso", { mode: "boolean" }), + emailWhitelistEnabled: integer("emailWhitelistEnabled", { + mode: "boolean" + }), + applyRules: integer("applyRules", { mode: "boolean" }), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), stickySession: integer("stickySession", { mode: "boolean" }) .notNull() diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index 5c6d50a85..104e02d5c 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -157,7 +157,9 @@ function getOpenApiDocumentation() { content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z + .record(z.string(), z.any()) + .nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index 45434aac4..f0e6dc95a 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -31,7 +31,7 @@ export enum TierFeature { } export const tierMatrix: Record = { - [TierFeature.Labels]: ["tier2", "tier3", "enterprise"], + [TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"], @@ -71,16 +71,6 @@ export const tierMatrix: Record = { [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.ResourcePolicies]: ["tier3", "enterprise"], - [TierFeature.AdvancedPublicResources]: [ - "tier1", - "tier2", - "tier3", - "enterprise" - ], - [TierFeature.AdvancedPrivateResources]: [ - "tier1", - "tier2", - "tier3", - "enterprise" - ] + [TierFeature.AdvancedPublicResources]: ["tier3", "enterprise"], + [TierFeature.AdvancedPrivateResources]: ["tier3", "enterprise"] }; diff --git a/server/lib/blueprints/privateResources.ts b/server/lib/blueprints/privateResources.ts index 3e6a784e0..8d00701f1 100644 --- a/server/lib/blueprints/privateResources.ts +++ b/server/lib/blueprints/privateResources.ts @@ -415,7 +415,11 @@ export async function updatePrivateResources( } else { let aliasAddress: string | null = null; let releaseAliasLock: (() => Promise) | null = null; - if (resourceData.mode === "host" || resourceData.mode === "http") { + if ( + resourceData.mode === "host" || + resourceData.mode === "http" || + resourceData.mode === "ssh" + ) { const { value, release } = await getNextAvailableAliasAddress( orgId, trx diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts index 2bc1a6d7f..b60970310 100644 --- a/server/lib/blueprints/publicResources.ts +++ b/server/lib/blueprints/publicResources.ts @@ -1467,17 +1467,6 @@ async function syncWhitelistUsers( .where(eq(resourceWhitelist.resourceId, resourceId)); for (const email of whitelistUsers) { - const [user] = await trx - .select() - .from(users) - .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (!user) { - throw new Error(`User not found: ${email} in org ${orgId}`); - } - const existingWhitelistEntry = existingWhitelist.find( (w) => w.email === email ); diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index a98843a99..5495a2d8e 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -1,8 +1,23 @@ import { z } from "zod"; +import { existsSync } from "node:fs"; import { portRangeStringSchema } from "@server/lib/ip"; import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema"; import { isValidRegionId } from "@server/db/regions"; import { wildcardSubdomainSchema } from "@server/lib/schemas"; +import config from "@server/lib/config"; + +const maxmindDbPath = config.getRawConfig().server.maxmind_db_path; +const maxmindAsnPath = config.getRawConfig().server.maxmind_asn_path; + +const hasMaxmindCountryDb = + typeof maxmindDbPath === "string" && + maxmindDbPath.length > 0 && + existsSync(maxmindDbPath); + +const hasMaxmindAsnDb = + typeof maxmindAsnPath === "string" && + maxmindAsnPath.length > 0 && + existsSync(maxmindAsnPath); export const SiteSchema = z.object({ name: z.string().min(1).max(100), @@ -117,6 +132,9 @@ export const RuleSchema = z .refine( (rule) => { if (rule.match === "country") { + if (!hasMaxmindCountryDb) { + return false; + } // Check if it's a valid 2-letter country code or "ALL" return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL"; } @@ -125,12 +143,15 @@ export const RuleSchema = z { path: ["value"], message: - "Value must be a 2-letter country code or 'ALL' when match is 'country'" + "Country rules require a valid existing server.maxmind_db_path and value must be a 2-letter country code or 'ALL'" } ) .refine( (rule) => { if (rule.match === "asn") { + if (!hasMaxmindCountryDb || !hasMaxmindAsnDb) { + return false; + } // Check if it's either AS format or "ALL" const asNumberPattern = /^AS\d+$/i; return asNumberPattern.test(rule.value) || rule.value === "ALL"; @@ -140,7 +161,7 @@ export const RuleSchema = z { path: ["value"], message: - "Value must be 'AS' format or 'ALL' when match is 'asn'" + "ASN rules require valid existing server.maxmind_db_path and server.maxmind_asn_path, and value must be 'AS' format or 'ALL'" } ) .refine( diff --git a/server/lib/ip.ts b/server/lib/ip.ts index d7e85e7ac..6da8bf887 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -504,7 +504,7 @@ export function generateRemoteSubnets( const parseResult = cidrSchema.safeParse(sr.destination); return parseResult.success; } - if (sr.mode === "host") { + if (sr.mode === "host" || sr.mode === "ssh") { // check if its a valid IP using zod const ipSchema = z.union([z.ipv4(), z.ipv6()]); const parseResult = ipSchema.safeParse(sr.destination); @@ -514,7 +514,7 @@ export function generateRemoteSubnets( }) .map((sr) => { if (sr.mode === "cidr") return sr.destination; - if (sr.mode === "host") { + if (sr.mode === "host" || sr.mode === "ssh") { return `${sr.destination}/32`; } return ""; // This should never be reached due to filtering, but satisfies TypeScript @@ -531,7 +531,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { .filter( (sr) => sr.aliasAddress && - ((sr.alias && sr.mode == "host") || + ((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) || (sr.fullDomain && sr.mode == "http")) ) .map((sr) => ({ @@ -577,6 +577,10 @@ export function generateSubnetProxyTargets( continue; } + if (!siteResource.destination) { + continue; + } + const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; const portRange = [ ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), @@ -584,7 +588,7 @@ export function generateSubnetProxyTargets( ]; const disableIcmp = siteResource.disableIcmp ?? false; - if (siteResource.mode == "host") { + if (siteResource.mode == "host" || siteResource.mode == "ssh") { let destination = siteResource.destination; // check if this is a valid ip const ipSchema = z.union([z.ipv4(), z.ipv6()]); @@ -665,6 +669,11 @@ export async function generateSubnetProxyTargetV2( return; } + if (!siteResource.destination) { + // ssh can have no destination + return; + } + const targets: SubnetProxyTargetV2[] = []; const portRange = [ @@ -673,7 +682,7 @@ export async function generateSubnetProxyTargetV2( ]; const disableIcmp = siteResource.disableIcmp ?? false; - if (siteResource.mode == "host") { + if (siteResource.mode == "host" || siteResource.mode == "ssh") { let destination = siteResource.destination; // check if this is a valid ip const ipSchema = z.union([z.ipv4(), z.ipv6()]); diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index 77f7740c5..4f1adbd53 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -181,6 +181,7 @@ class TelemetryClient { let numPrivResourceHosts = 0; let numPrivResourceCidr = 0; let numPrivResourceHttp = 0; + let numPrivResourceSsh = 0; for (const res of allPrivateResources) { if (res.mode === "host") { numPrivResourceHosts += 1; @@ -188,6 +189,8 @@ class TelemetryClient { numPrivResourceCidr += 1; } else if (res.mode === "http") { numPrivResourceHttp += 1; + } else if (res.mode === "ssh") { + numPrivResourceSsh += 1; } if (res.alias) { @@ -207,6 +210,7 @@ class TelemetryClient { numPrivateResourceHosts: numPrivResourceHosts, numPrivateResourceCidr: numPrivResourceCidr, numPrivateResourceHttp: numPrivResourceHttp, + numPrivateResourceSsh: numPrivResourceSsh, numAlertRules: numAlertRules.count, numUserDevices: userDevicesCount.count, numMachineClients: machineClients.count, diff --git a/server/middlewares/verifyResourceAccess.ts b/server/middlewares/verifyResourceAccess.ts index ba49f02e3..f790a481a 100644 --- a/server/middlewares/verifyResourceAccess.ts +++ b/server/middlewares/verifyResourceAccess.ts @@ -1,11 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { db, Resource } from "@server/db"; -import { resources, userOrgs, userResources, roleResources } from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { resources, userOrgs } from "@server/db"; +import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { + getRoleResourceAccess, + getUserResourceAccess +} from "@server/db/queries/verifySessionQueries"; export async function verifyResourceAccess( req: Request, @@ -116,37 +120,22 @@ export async function verifyResourceAccess( const roleResourceAccess = (req.userOrgRoleIds?.length ?? 0) > 0 - ? await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resource.resourceId), - inArray( - roleResources.roleId, - req.userOrgRoleIds! - ) - ) - ) - .limit(1) - : []; + ? await getRoleResourceAccess( + resource.resourceId, + req.userOrgRoleIds! + ) + : null; - if (roleResourceAccess.length > 0) { + if (roleResourceAccess) { return next(); } - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resource.resourceId) - ) - ) - .limit(1); + const userResourceAccess = await getUserResourceAccess( + userId, + resource.resourceId + ); - if (userResourceAccess.length > 0) { + if (userResourceAccess) { return next(); } diff --git a/server/private/routers/alertRule/createAlertRule.ts b/server/private/routers/alertRule/createAlertRule.ts index f4a11ad3d..7a46c84ff 100644 --- a/server/private/routers/alertRule/createAlertRule.ts +++ b/server/private/routers/alertRule/createAlertRule.ts @@ -208,7 +208,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/alertRule/deleteAlertRule.ts b/server/private/routers/alertRule/deleteAlertRule.ts index b475bb6c3..0439a6622 100644 --- a/server/private/routers/alertRule/deleteAlertRule.ts +++ b/server/private/routers/alertRule/deleteAlertRule.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -112,4 +112,4 @@ export async function deleteAlertRule( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/alertRule/getAlertRule.ts b/server/private/routers/alertRule/getAlertRule.ts index dde9093fb..fcbd44f11 100644 --- a/server/private/routers/alertRule/getAlertRule.ts +++ b/server/private/routers/alertRule/getAlertRule.ts @@ -32,7 +32,10 @@ import { OpenAPITags, registry } from "@server/openApi"; import { and, eq } from "drizzle-orm"; import { decrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import { GetAlertRuleResponse, WebhookAlertConfig } from "@server/routers/alertRule/types"; +import { + GetAlertRuleResponse, + WebhookAlertConfig +} from "@server/routers/alertRule/types"; const paramsSchema = z .object({ @@ -55,7 +58,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/alertRule/listAlertRules.ts b/server/private/routers/alertRule/listAlertRules.ts index 3931da44c..0fc720d66 100644 --- a/server/private/routers/alertRule/listAlertRules.ts +++ b/server/private/routers/alertRule/listAlertRules.ts @@ -101,7 +101,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auditLogs/exportAccessAuditLog.ts b/server/private/routers/auditLogs/exportAccessAuditLog.ts index b83673b33..5c6240b2e 100644 --- a/server/private/routers/auditLogs/exportAccessAuditLog.ts +++ b/server/private/routers/auditLogs/exportAccessAuditLog.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auditLogs/exportActionAuditLog.ts b/server/private/routers/auditLogs/exportActionAuditLog.ts index 0d707c41e..112f03fb4 100644 --- a/server/private/routers/auditLogs/exportActionAuditLog.ts +++ b/server/private/routers/auditLogs/exportActionAuditLog.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auditLogs/exportConnectionAuditLog.ts b/server/private/routers/auditLogs/exportConnectionAuditLog.ts index 1115d23ad..a20d4052a 100644 --- a/server/private/routers/auditLogs/exportConnectionAuditLog.ts +++ b/server/private/routers/auditLogs/exportConnectionAuditLog.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -72,7 +72,9 @@ export async function exportConnectionAuditLogs( ); } - const parsedParams = queryConnectionAuditLogsParams.safeParse(req.params); + const parsedParams = queryConnectionAuditLogsParams.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -112,4 +114,4 @@ export async function exportConnectionAuditLogs( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 570621216..0feca4154 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -11,7 +11,14 @@ * This file is not licensed under the AGPLv3. */ -import { accessAuditLog, logsDb, resources, siteResources, db, primaryDb } from "@server/db"; +import { + accessAuditLog, + logsDb, + resources, + siteResources, + db, + primaryDb +} from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -150,21 +157,30 @@ export function queryAccess(data: Q) { .orderBy(desc(accessAuditLog.timestamp), desc(accessAuditLog.id)); } -async function enrichWithResourceDetails(logs: Awaited>) { +async function enrichWithResourceDetails( + logs: Awaited> +) { const resourceIds = logs - .map(log => log.resourceId) + .map((log) => log.resourceId) .filter((id): id is number => id !== null && id !== undefined); const siteResourceIds = logs - .filter(log => log.resourceId == null && log.siteResourceId != null) - .map(log => log.siteResourceId) + .filter((log) => log.resourceId == null && log.siteResourceId != null) + .map((log) => log.siteResourceId) .filter((id): id is number => id !== null && id !== undefined); if (resourceIds.length === 0 && siteResourceIds.length === 0) { - return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null })); + return logs.map((log) => ({ + ...log, + resourceName: null, + resourceNiceId: null + })); } - const resourceMap = new Map(); + const resourceMap = new Map< + number, + { name: string | null; niceId: string | null } + >(); if (resourceIds.length > 0) { const resourceDetails = await primaryDb @@ -181,7 +197,10 @@ async function enrichWithResourceDetails(logs: Awaited(); + const siteResourceMap = new Map< + number, + { name: string | null; niceId: string | null } + >(); if (siteResourceIds.length > 0) { const siteResourceDetails = await primaryDb @@ -194,12 +213,15 @@ async function enrichWithResourceDetails(logs: Awaited { + return logs.map((log) => { if (log.resourceId != null) { const details = resourceMap.get(log.resourceId); return { @@ -273,11 +295,11 @@ async function queryUniqueFilterAttributes( // Fetch resource names from main database for the unique resource IDs const resourceIds = uniqueResources - .map(row => row.id) + .map((row) => row.id) .filter((id): id is number => id !== null); const siteResourceIds = uniqueSiteResources - .map(row => row.id) + .map((row) => row.id) .filter((id): id is number => id !== null); let resourcesWithNames: Array<{ id: number; name: string | null }> = []; @@ -293,7 +315,7 @@ async function queryUniqueFilterAttributes( resourcesWithNames = [ ...resourcesWithNames, - ...resourceDetails.map(r => ({ + ...resourceDetails.map((r) => ({ id: r.resourceId, name: r.name })) @@ -311,7 +333,7 @@ async function queryUniqueFilterAttributes( resourcesWithNames = [ ...resourcesWithNames, - ...siteResourceDetails.map(r => ({ + ...siteResourceDetails.map((r) => ({ id: r.siteResourceId, name: r.name })) @@ -344,7 +366,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 56f2f0201..271929875 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -171,7 +171,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auditLogs/queryConnectionAuditLog.ts b/server/private/routers/auditLogs/queryConnectionAuditLog.ts index e6dd2f6a5..e214d0c66 100644 --- a/server/private/routers/auditLogs/queryConnectionAuditLog.ts +++ b/server/private/routers/auditLogs/queryConnectionAuditLog.ts @@ -459,7 +459,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/auth/transferSession.ts b/server/private/routers/auth/transferSession.ts index 78673e8a0..d26664dfd 100644 --- a/server/private/routers/auth/transferSession.ts +++ b/server/private/routers/auth/transferSession.ts @@ -17,8 +17,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -import { sessions, sessionTransferToken } from "@server/db"; -import { db } from "@server/db"; +import { db, safeRead, sessions, sessionTransferToken } from "@server/db"; import { eq } from "drizzle-orm"; import { response } from "@server/lib/response"; import { encodeHexLowerCase } from "@oslojs/encoding"; @@ -57,15 +56,19 @@ export async function transferSession( sha256(new TextEncoder().encode(token)) ); - const [existing] = await db - .select() - .from(sessionTransferToken) - .where(eq(sessionTransferToken.token, tokenRaw)) - .innerJoin( - sessions, - eq(sessions.sessionId, sessionTransferToken.sessionId) - ) - .limit(1); + const result = await safeRead((db) => + db + .select() + .from(sessionTransferToken) + .where(eq(sessionTransferToken.token, tokenRaw)) + .innerJoin( + sessions, + eq(sessions.sessionId, sessionTransferToken.sessionId) + ) + .limit(1) + ); + + const [existing] = result; if (!existing) { return next( diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index ad2102df5..718559654 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -45,7 +45,7 @@ const getOrgSchema = z.strictObject({ // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/private/routers/certificates/getCertificate.ts b/server/private/routers/certificates/getCertificate.ts index c365d3d7b..60a6de59f 100644 --- a/server/private/routers/certificates/getCertificate.ts +++ b/server/private/routers/certificates/getCertificate.ts @@ -121,7 +121,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/certificates/restartCertificate.ts b/server/private/routers/certificates/restartCertificate.ts index 50c747f7b..9c3cbb8cc 100644 --- a/server/private/routers/certificates/restartCertificate.ts +++ b/server/private/routers/certificates/restartCertificate.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts index c5320c1f6..b2bfb8923 100644 --- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -29,7 +29,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix"; const paramsSchema = z.strictObject({}); const querySchema = z.strictObject({ - subdomain: z.string(), + subdomain: z.string() // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise }); @@ -48,7 +48,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts index 13b54ef17..5aa4bf314 100644 --- a/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts +++ b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts @@ -33,7 +33,8 @@ const paramsSchema = z registry.registerPath({ method: "delete", path: "/org/{orgId}/event-streaming-destination/{destinationId}", - description: "Delete an event streaming destination for a specific organization.", + description: + "Delete an event streaming destination for a specific organization.", tags: [OpenAPITags.Org], request: { params: paramsSchema @@ -44,7 +45,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -115,4 +116,4 @@ export async function deleteEventStreamingDestination( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/healthChecks/deleteHealthCheck.ts b/server/private/routers/healthChecks/deleteHealthCheck.ts index b5d054783..90bb41e50 100644 --- a/server/private/routers/healthChecks/deleteHealthCheck.ts +++ b/server/private/routers/healthChecks/deleteHealthCheck.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/healthChecks/listHealthChecks.ts b/server/private/routers/healthChecks/listHealthChecks.ts index c0198d96e..0d18b881e 100644 --- a/server/private/routers/healthChecks/listHealthChecks.ts +++ b/server/private/routers/healthChecks/listHealthChecks.ts @@ -74,7 +74,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 445b6f318..c6be3e7d1 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -219,9 +219,9 @@ export type ResourceWithAuth = { password: ResourcePassword | ResourcePolicyPassword | null; headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; - applyRules: boolean; - sso: boolean; - emailWhitelistEnabled: boolean; + applyRules: boolean | null; + sso: boolean | null; + emailWhitelistEnabled: boolean | null; org: Org; }; diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index c829f0c84..8b214b895 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -69,7 +69,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -127,7 +127,8 @@ export async function createOrgOidcIdp( let { autoProvision } = parsedBody.data; - if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted + if (build == "saas") { + // this is not paywalled with a ee license because this whole endpoint is restricted const subscribed = await isSubscribed( orgId, tierMatrix.deviceApprovals diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 2007f180e..f7cfd82bf 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts index a2f29a57f..e22929d46 100644 --- a/server/private/routers/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -62,7 +62,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts index 672fbc054..66a2da2fe 100644 --- a/server/private/routers/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -78,7 +78,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/user/addUserRole.ts b/server/private/routers/user/addUserRole.ts index a59993a5d..c59a3d0f7 100644 --- a/server/private/routers/user/addUserRole.ts +++ b/server/private/routers/user/addUserRole.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/private/routers/user/removeUserRole.ts b/server/private/routers/user/removeUserRole.ts index de099be6d..b96670815 100644 --- a/server/private/routers/user/removeUserRole.ts +++ b/server/private/routers/user/removeUserRole.ts @@ -45,7 +45,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/accessToken/deleteAccessToken.ts b/server/routers/accessToken/deleteAccessToken.ts index 405d4e68e..82344630c 100644 --- a/server/routers/accessToken/deleteAccessToken.ts +++ b/server/routers/accessToken/deleteAccessToken.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index fd03bc06d..a06068c01 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -61,7 +61,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index 88ff07b27..0339cc2c4 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -135,7 +135,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -164,7 +164,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts index 07d00d727..895ebf560 100644 --- a/server/routers/apiKeys/deleteApiKey.ts +++ b/server/routers/apiKeys/deleteApiKey.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts index e31e8b8b9..62c06ba73 100644 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -42,7 +42,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/auditLogs/exportRequestAuditLog.ts b/server/routers/auditLogs/exportRequestAuditLog.ts index 1b4eede00..156f85113 100644 --- a/server/routers/auditLogs/exportRequestAuditLog.ts +++ b/server/routers/auditLogs/exportRequestAuditLog.ts @@ -35,7 +35,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index 62b0bd75e..7f8aaf047 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -162,7 +162,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 14bbb3c4d..f14c28cf1 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -1,4 +1,11 @@ -import { logsDb, requestAuditLog, resources, siteResources, db, primaryDb } from "@server/db"; +import { + logsDb, + requestAuditLog, + resources, + siteResources, + db, + primaryDb +} from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -127,16 +134,16 @@ export function queryRequest(data: Q) { return logsDb .select({ id: requestAuditLog.id, - timestamp: requestAuditLog.timestamp, - orgId: requestAuditLog.orgId, - action: requestAuditLog.action, - reason: requestAuditLog.reason, - actorType: requestAuditLog.actorType, - actor: requestAuditLog.actor, - actorId: requestAuditLog.actorId, - resourceId: requestAuditLog.resourceId, - siteResourceId: requestAuditLog.siteResourceId, - ip: requestAuditLog.ip, + timestamp: requestAuditLog.timestamp, + orgId: requestAuditLog.orgId, + action: requestAuditLog.action, + reason: requestAuditLog.reason, + actorType: requestAuditLog.actorType, + actor: requestAuditLog.actor, + actorId: requestAuditLog.actorId, + resourceId: requestAuditLog.resourceId, + siteResourceId: requestAuditLog.siteResourceId, + ip: requestAuditLog.ip, location: requestAuditLog.location, userAgent: requestAuditLog.userAgent, metadata: requestAuditLog.metadata, @@ -154,21 +161,30 @@ export function queryRequest(data: Q) { .orderBy(desc(requestAuditLog.timestamp)); } -async function enrichWithResourceDetails(logs: Awaited>) { +async function enrichWithResourceDetails( + logs: Awaited> +) { const resourceIds = logs - .map(log => log.resourceId) + .map((log) => log.resourceId) .filter((id): id is number => id !== null && id !== undefined); const siteResourceIds = logs - .filter(log => log.resourceId == null && log.siteResourceId != null) - .map(log => log.siteResourceId) + .filter((log) => log.resourceId == null && log.siteResourceId != null) + .map((log) => log.siteResourceId) .filter((id): id is number => id !== null && id !== undefined); if (resourceIds.length === 0 && siteResourceIds.length === 0) { - return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null })); + return logs.map((log) => ({ + ...log, + resourceName: null, + resourceNiceId: null + })); } - const resourceMap = new Map(); + const resourceMap = new Map< + number, + { name: string | null; niceId: string | null } + >(); if (resourceIds.length > 0) { const resourceDetails = await primaryDb @@ -185,7 +201,10 @@ async function enrichWithResourceDetails(logs: Awaited(); + const siteResourceMap = new Map< + number, + { name: string | null; niceId: string | null } + >(); if (siteResourceIds.length > 0) { const siteResourceDetails = await primaryDb @@ -198,12 +217,15 @@ async function enrichWithResourceDetails(logs: Awaited { + return logs.map((log) => { if (log.resourceId != null) { const details = resourceMap.get(log.resourceId); return { @@ -247,7 +269,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -333,11 +355,11 @@ async function queryUniqueFilterAttributes( // Fetch resource names from main database for the unique resource IDs const resourceIds = uniqueResources - .map(row => row.id) + .map((row) => row.id) .filter((id): id is number => id !== null); const siteResourceIds = uniqueSiteResources - .map(row => row.id) + .map((row) => row.id) .filter((id): id is number => id !== null); let resourcesWithNames: Array<{ id: number; name: string | null }> = []; @@ -353,7 +375,7 @@ async function queryUniqueFilterAttributes( resourcesWithNames = [ ...resourcesWithNames, - ...resourceDetails.map(r => ({ + ...resourceDetails.map((r) => ({ id: r.resourceId, name: r.name })) @@ -371,7 +393,7 @@ async function queryUniqueFilterAttributes( resourcesWithNames = [ ...resourcesWithNames, - ...siteResourceDetails.map(r => ({ + ...siteResourceDetails.map((r) => ({ id: r.siteResourceId, name: r.name })) diff --git a/server/routers/auth/lookupUser.ts b/server/routers/auth/lookupUser.ts index d086cf4e1..dc32d5bb6 100644 --- a/server/routers/auth/lookupUser.ts +++ b/server/routers/auth/lookupUser.ts @@ -1,14 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { - users, - userOrgs, - orgs, - idpOrg, - idp, - idpOidcConfig -} from "@server/db"; +import { users, userOrgs, orgs, idpOrg, idp, idpOidcConfig } from "@server/db"; import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -57,7 +50,7 @@ export type LookupUserResponse = { // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), @@ -169,46 +162,54 @@ export async function lookupUser( ); // Deduplicate orgs (user might have multiple memberships in same org) - const uniqueOrgs = new Map(); + const uniqueOrgs = new Map< + string, + (typeof userOrgMemberships)[0] + >(); for (const membership of userOrgMemberships) { if (!uniqueOrgs.has(membership.orgId)) { uniqueOrgs.set(membership.orgId, membership); } } - const orgsData = Array.from(uniqueOrgs.values()).map((membership) => { - // Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP - // Only show IdPs where the user's idpId matches - // Internal users don't have an idpId, so they won't see any IdPs - const orgIdpsList = orgIdps - .filter((idp) => { - if (idp.orgId !== membership.orgId) { + const orgsData = Array.from(uniqueOrgs.values()).map( + (membership) => { + // Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP + // Only show IdPs where the user's idpId matches + // Internal users don't have an idpId, so they won't see any IdPs + const orgIdpsList = orgIdps + .filter((idp) => { + if (idp.orgId !== membership.orgId) { + return false; + } + // Only show IdPs where the user (with exact identifier) is authenticated via that IdP + // This means user.idpId must match idp.idpId + if ( + user.idpId !== null && + user.idpId === idp.idpId + ) { + return true; + } return false; - } - // Only show IdPs where the user (with exact identifier) is authenticated via that IdP - // This means user.idpId must match idp.idpId - if (user.idpId !== null && user.idpId === idp.idpId) { - return true; - } - return false; - }) - .map((idp) => ({ - idpId: idp.idpId, - name: idp.idpName, - variant: idp.variant - })); + }) + .map((idp) => ({ + idpId: idp.idpId, + name: idp.idpName, + variant: idp.variant + })); - // Check if user has internal auth for this org - // User has internal auth if they have an internal account type - const orgHasInternalAuth = hasInternalAuth; + // Check if user has internal auth for this org + // User has internal auth if they have an internal account type + const orgHasInternalAuth = hasInternalAuth; - return { - orgId: membership.orgId, - orgName: membership.orgName, - idps: orgIdpsList, - hasInternalAuth: orgHasInternalAuth - }; - }); + return { + orgId: membership.orgId, + orgName: membership.orgName, + idps: orgIdpsList, + hasInternalAuth: orgHasInternalAuth + }; + } + ); accounts.push({ userId: user.userId, diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 0f1b1c23c..677fa281d 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -145,9 +145,9 @@ export async function verifyResourceSession( | ResourcePolicyHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; - applyRules: boolean; - sso: boolean; - emailWhitelistEnabled: boolean; + applyRules: boolean | null; + sso: boolean | null; + emailWhitelistEnabled: boolean | null; org: Org; } | undefined = localCache.get(resourceCacheKey); diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts index cd7026a9a..8ad41e9e4 100644 --- a/server/routers/blueprints/applyJSONBlueprint.ts +++ b/server/routers/blueprints/applyJSONBlueprint.ts @@ -37,7 +37,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/blueprints/applyYAMLBlueprint.ts b/server/routers/blueprints/applyYAMLBlueprint.ts index 5ddebeb67..9809e6971 100644 --- a/server/routers/blueprints/applyYAMLBlueprint.ts +++ b/server/routers/blueprints/applyYAMLBlueprint.ts @@ -60,7 +60,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/blueprints/getBlueprint.ts b/server/routers/blueprints/getBlueprint.ts index 4bad8ee3f..9f56c04d5 100644 --- a/server/routers/blueprints/getBlueprint.ts +++ b/server/routers/blueprints/getBlueprint.ts @@ -62,7 +62,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/blueprints/listBlueprints.ts b/server/routers/blueprints/listBlueprints.ts index f94bd9978..ab9ac604e 100644 --- a/server/routers/blueprints/listBlueprints.ts +++ b/server/routers/blueprints/listBlueprints.ts @@ -80,7 +80,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/archiveClient.ts b/server/routers/client/archiveClient.ts index 25ea06f7d..9d6254b8f 100644 --- a/server/routers/client/archiveClient.ts +++ b/server/routers/client/archiveClient.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/blockClient.ts b/server/routers/client/blockClient.ts index 1a7cb48c4..659cd864f 100644 --- a/server/routers/client/blockClient.ts +++ b/server/routers/client/blockClient.ts @@ -30,7 +30,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -94,7 +94,11 @@ export async function blockClient( // Send terminate signal if there's an associated OLM and it's connected if (client.olmId && client.online) { - await sendTerminateClient(client.clientId, OlmErrorCodes.TERMINATED_BLOCKED, client.olmId); + await sendTerminateClient( + client.clientId, + OlmErrorCodes.TERMINATED_BLOCKED, + client.olmId + ); } }); diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index ddf8f3dbf..ecda098c5 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -65,7 +65,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts index c51c4fe6a..09bec218a 100644 --- a/server/routers/client/createUserClient.ts +++ b/server/routers/client/createUserClient.ts @@ -66,7 +66,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index 21d6f1c9f..24ab9917a 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -31,7 +31,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 936d62b7d..21b49cf7a 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -259,7 +259,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -287,7 +287,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -340,18 +340,18 @@ export async function getClient( // Build fingerprint data if available const fingerprintData = client.currentFingerprint ? { - username: client.currentFingerprint.username || null, - hostname: client.currentFingerprint.hostname || null, - platform: client.currentFingerprint.platform || null, - osVersion: client.currentFingerprint.osVersion || null, - kernelVersion: - client.currentFingerprint.kernelVersion || null, - arch: client.currentFingerprint.arch || null, - deviceModel: client.currentFingerprint.deviceModel || null, - serialNumber: client.currentFingerprint.serialNumber || null, - firstSeen: client.currentFingerprint.firstSeen || null, - lastSeen: client.currentFingerprint.lastSeen || null - } + username: client.currentFingerprint.username || null, + hostname: client.currentFingerprint.hostname || null, + platform: client.currentFingerprint.platform || null, + osVersion: client.currentFingerprint.osVersion || null, + kernelVersion: + client.currentFingerprint.kernelVersion || null, + arch: client.currentFingerprint.arch || null, + deviceModel: client.currentFingerprint.deviceModel || null, + serialNumber: client.currentFingerprint.serialNumber || null, + firstSeen: client.currentFingerprint.firstSeen || null, + lastSeen: client.currentFingerprint.lastSeen || null + } : null; // Build posture data if available (platform-specific) diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 1fffdcba9..9178c27a5 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -218,7 +218,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts index f9af3af78..e2a035929 100644 --- a/server/routers/client/listUserDevices.ts +++ b/server/routers/client/listUserDevices.ts @@ -219,7 +219,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index b2d49db4c..c208acd88 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -13,7 +13,7 @@ import semver from "semver"; const NEWT_V2_TARGETS_VERSION = ">=1.10.3"; -export async function convertTargetsIfNessicary( +export async function convertTargetsIfNecessary( newtId: string, targets: SubnetProxyTarget[] | SubnetProxyTargetV2[] ) { @@ -47,7 +47,7 @@ export async function addTargets( targets: SubnetProxyTarget[] | SubnetProxyTargetV2[], version?: string | null ) { - targets = await convertTargetsIfNessicary(newtId, targets); + targets = await convertTargetsIfNecessary(newtId, targets); await sendToClient( newtId, @@ -64,7 +64,7 @@ export async function removeTargets( targets: SubnetProxyTarget[] | SubnetProxyTargetV2[], version?: string | null ) { - targets = await convertTargetsIfNessicary(newtId, targets); + targets = await convertTargetsIfNecessary(newtId, targets); await sendToClient( newtId, diff --git a/server/routers/client/unarchiveClient.ts b/server/routers/client/unarchiveClient.ts index e8b3aef5a..4ca2503ca 100644 --- a/server/routers/client/unarchiveClient.ts +++ b/server/routers/client/unarchiveClient.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/unblockClient.ts b/server/routers/client/unblockClient.ts index fa02394a5..f0c1cff1d 100644 --- a/server/routers/client/unblockClient.ts +++ b/server/routers/client/unblockClient.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index f8794960c..2c1eccd48 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -42,7 +42,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/domain/getDNSRecords.ts b/server/routers/domain/getDNSRecords.ts index 8a557c6cf..a064d5c9f 100644 --- a/server/routers/domain/getDNSRecords.ts +++ b/server/routers/domain/getDNSRecords.ts @@ -43,7 +43,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts index 3c484505b..9c5d2bd88 100644 --- a/server/routers/domain/getDomain.ts +++ b/server/routers/domain/getDomain.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index e6d330263..e2447a42c 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -31,7 +31,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index 716de4a9d..27842404b 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -29,7 +29,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index 23f7990d5..10aa71c8b 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 8188f46da..6a82f2c12 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Org } from "@server/db"; +import { db, Org, primaryDb } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -635,9 +635,7 @@ export async function validateOidcCallback( } }); - db.transaction(async (trx) => { - await calculateUserClientsForOrgs(userId!, trx); - }).catch((err) => { + calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => { logger.error( "Error calculating user clients after syncing orgs and roles for OIDC user", { error: err } diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index 73bf2c630..154ab38cb 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -152,34 +152,64 @@ export async function buildClientConfigurationForNewtClient( const targetsToSend: SubnetProxyTargetV2[] = []; - for (const resource of allSiteResources) { - // Get clients associated with this specific resource - const resourceClients = await db - .select({ - clientId: clients.clientId, - pubKey: clients.pubKey, - subnet: clients.subnet - }) - .from(clients) - .innerJoin( - clientSiteResourcesAssociationsCache, - eq( - clients.clientId, - clientSiteResourcesAssociationsCache.clientId - ) - ) - .where( - eq( - clientSiteResourcesAssociationsCache.siteResourceId, - resource.siteResourceId - ) - ); + if (allSiteResources.length === 0) { + return { + peers: validPeers, + targets: targetsToSend + }; + } - const resourceTargets = await generateSubnetProxyTargetV2( - resource, - resourceClients + // Batch fetch all client associations for every site resource in one query + // to avoid an N+1 lookup that would issue thousands of queries when a site + // has many resources. + const siteResourceIds = allSiteResources.map((r) => r.siteResourceId); + + const resourceClientRows = await db + .select({ + siteResourceId: clientSiteResourcesAssociationsCache.siteResourceId, + clientId: clients.clientId, + pubKey: clients.pubKey, + subnet: clients.subnet + }) + .from(clients) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq(clients.clientId, clientSiteResourcesAssociationsCache.clientId) + ) + .where( + inArray( + clientSiteResourcesAssociationsCache.siteResourceId, + siteResourceIds + ) ); + const clientsByResourceId = new Map< + number, + { clientId: number; pubKey: string | null; subnet: string | null }[] + >(); + for (const row of resourceClientRows) { + let list = clientsByResourceId.get(row.siteResourceId); + if (!list) { + list = []; + clientsByResourceId.set(row.siteResourceId, list); + } + list.push({ + clientId: row.clientId, + pubKey: row.pubKey, + subnet: row.subnet + }); + } + + const resourceTargetsArr = await Promise.all( + allSiteResources.map((resource) => + generateSubnetProxyTargetV2( + resource, + clientsByResourceId.get(resource.siteResourceId) ?? [] + ) + ) + ); + + for (const resourceTargets of resourceTargetsArr) { if (resourceTargets) { targetsToSend.push(...resourceTargets); } diff --git a/server/routers/newt/getNewtVersion.ts b/server/routers/newt/getNewtVersion.ts index 0dd0eff18..8a76bc3d2 100644 --- a/server/routers/newt/getNewtVersion.ts +++ b/server/routers/newt/getNewtVersion.ts @@ -56,13 +56,18 @@ async function getLatestReleaseInfo(): Promise { return staleReleaseInfo; } - // Drop drafts, pre-releases, and anything with "rc" in the tag name. + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + // Drop drafts, pre-releases, anything with "rc" in the tag name, + // and releases published less than 1 day ago. releases = releases.filter( (r: any) => !r.draft && !r.prerelease && !r.tag_name.includes("rc") && - !r.tag_name.includes("v") + !r.tag_name.includes("v") && + r.published_at && + new Date(r.published_at) <= oneDayAgo ); // Sort descending by semver to find the true latest stable release. diff --git a/server/routers/newt/handleNewtGetConfigMessage.ts b/server/routers/newt/handleNewtGetConfigMessage.ts index 787151a5a..d78fa6f71 100644 --- a/server/routers/newt/handleNewtGetConfigMessage.ts +++ b/server/routers/newt/handleNewtGetConfigMessage.ts @@ -6,7 +6,7 @@ import { db, ExitNode, exitNodes, Newt, sites } from "@server/db"; import { eq } from "drizzle-orm"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { buildClientConfigurationForNewtClient } from "./buildConfiguration"; -import { convertTargetsIfNessicary } from "../client/targets"; +import { convertTargetsIfNecessary } from "../client/targets"; import { canCompress } from "@server/lib/clientVersionChecks"; import config from "@server/lib/config"; @@ -113,7 +113,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => { exitNode ); - const targetsToSend = await convertTargetsIfNessicary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format + const targetsToSend = await convertTargetsIfNecessary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format return { message: { diff --git a/server/routers/olm/createUserOlm.ts b/server/routers/olm/createUserOlm.ts index 486546ada..714fb4b35 100644 --- a/server/routers/olm/createUserOlm.ts +++ b/server/routers/olm/createUserOlm.ts @@ -49,7 +49,7 @@ export type CreateOlmResponse = { // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/routers/olm/deleteUserOlm.ts b/server/routers/olm/deleteUserOlm.ts index df9328dd3..861a413d8 100644 --- a/server/routers/olm/deleteUserOlm.ts +++ b/server/routers/olm/deleteUserOlm.ts @@ -34,7 +34,7 @@ const paramsSchema = z // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/routers/olm/getUserOlm.ts b/server/routers/olm/getUserOlm.ts index 534ae3bb7..b3f09a2b2 100644 --- a/server/routers/olm/getUserOlm.ts +++ b/server/routers/olm/getUserOlm.ts @@ -36,7 +36,7 @@ const querySchema = z.object({ // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/routers/olm/listUserOlms.ts b/server/routers/olm/listUserOlms.ts index d6ac26819..b2db262e6 100644 --- a/server/routers/olm/listUserOlms.ts +++ b/server/routers/olm/listUserOlms.ts @@ -47,7 +47,7 @@ const paramsSchema = z // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/routers/org/checkOrgUserAccess.ts b/server/routers/org/checkOrgUserAccess.ts index c40c85aaa..78afee701 100644 --- a/server/routers/org/checkOrgUserAccess.ts +++ b/server/routers/org/checkOrgUserAccess.ts @@ -49,10 +49,7 @@ async function queryUser(orgId: string, userId: string) { .from(userOrgRoles) .leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId)) .where( - and( - eq(userOrgRoles.userId, userId), - eq(userOrgRoles.orgId, orgId) - ) + and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId)) ); const isAdmin = roleRows.some((r) => r.isAdmin); @@ -89,7 +86,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index a195e3a33..7b2b1f87a 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -80,7 +80,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 5beffb536..a81b23504 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -30,7 +30,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index 465dd5801..c48f2fa91 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -43,7 +43,7 @@ const listOrgsSchema = z.object({ // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), diff --git a/server/routers/org/resetOrgBandwidth.ts b/server/routers/org/resetOrgBandwidth.ts index 41ad761e3..0e605dba1 100644 --- a/server/routers/org/resetOrgBandwidth.ts +++ b/server/routers/org/resetOrgBandwidth.ts @@ -27,7 +27,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 2f20d3760..f98bdec27 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -68,7 +68,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts index 1ad10000f..506c2caa9 100644 --- a/server/routers/resource/addEmailToResourceWhitelist.ts +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/addRoleToResource.ts b/server/routers/resource/addRoleToResource.ts index 5637cf8f8..a1d493631 100644 --- a/server/routers/resource/addRoleToResource.ts +++ b/server/routers/resource/addRoleToResource.ts @@ -28,7 +28,8 @@ const addRoleToResourceParamsSchema = z registry.registerPath({ method: "post", path: "/resource/{resourceId}/roles/add", - description: "Add a single role to a resource.", + description: + "Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.", tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: addRoleToResourceParamsSchema, @@ -46,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/addUserToResource.ts b/server/routers/resource/addUserToResource.ts index 0b749e04f..ebe43fccd 100644 --- a/server/routers/resource/addUserToResource.ts +++ b/server/routers/resource/addUserToResource.ts @@ -28,7 +28,8 @@ const addUserToResourceParamsSchema = z registry.registerPath({ method: "post", path: "/resource/{resourceId}/users/add", - description: "Add a single user to a resource.", + description: + "Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.", tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: addUserToResourceParamsSchema, @@ -46,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 0bb90e7b8..b9547bfbf 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -168,7 +168,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index 14ac69fcc..9d2261338 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -49,7 +49,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index fa8bd96bf..a959611ec 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -37,7 +37,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts index a3a1543f8..c619a693e 100644 --- a/server/routers/resource/deleteResourceRule.ts +++ b/server/routers/resource/deleteResourceRule.ts @@ -29,7 +29,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 3a650578c..708351db1 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -1,4 +1,4 @@ -import { db, resources } from "@server/db"; +import { db, resourcePolicies, resources } from "@server/db"; import response from "@server/lib/response"; import stoi from "@server/lib/stoi"; import logger from "@server/logger"; @@ -41,6 +41,15 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) { } } +async function queryInlinePolicy(resourcePolicyId: number) { + const [res] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + return res; +} + export type GetResourceResponse = Omit< NonNullable>>, "headers" @@ -66,7 +75,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -94,7 +103,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -132,12 +141,31 @@ export async function getResource( ); } + const isInlinePolicy = + resource.resourcePolicyId === null && + resource.defaultResourcePolicyId !== null; + + let returnData = resource; + if (isInlinePolicy) { + // get the policy + const policy = await queryInlinePolicy( + resource.defaultResourcePolicyId! + ); + returnData = { + ...returnData, + sso: policy?.sso || null, + emailWhitelistEnabled: policy?.emailWhitelistEnabled || null, + applyRules: policy?.applyRules || null, + skipToIdpId: policy?.idpId || null + }; + } + return response(res, { data: { - ...resource, - headers: resource.headers - ? JSON.parse(resource.headers) - : resource.headers + ...returnData, + headers: returnData.headers + ? JSON.parse(returnData.headers) + : returnData.headers }, success: true, error: false, diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index c773f89b9..2d882d49e 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -54,7 +54,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 8ba432fd6..c82dcac48 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -203,9 +203,9 @@ export async function getUserResources( fullDomain: string | null; ssl: boolean; enabled: boolean; - sso: boolean; + sso: boolean | null; mode: string; - emailWhitelistEnabled: boolean; + emailWhitelistEnabled: boolean | null; policyEmailWhitelistEnabled: boolean | null; }> = []; if (uniqueResourceIds.length > 0) { diff --git a/server/routers/resource/listAllResourceNames.ts b/server/routers/resource/listAllResourceNames.ts index 7c4f18b35..e38a3fd69 100644 --- a/server/routers/resource/listAllResourceNames.ts +++ b/server/routers/resource/listAllResourceNames.ts @@ -45,7 +45,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index 4ee3f9535..ea4d07f64 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -58,7 +58,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts index 76d6fb97e..6b9df688a 100644 --- a/server/routers/resource/listResourceRules.ts +++ b/server/routers/resource/listResourceRules.ts @@ -82,7 +82,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index 292edf3cd..c99dbf0ad 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -48,7 +48,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 8e0a03384..684c48159 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -123,6 +123,16 @@ const listResourcesSchema = z.object({ description: "Filter resources based on health status of their targets. `healthy` means all targets are healthy. `degraded` means at least one target is unhealthy, but not all are unhealthy. `offline` means all targets are unhealthy. `unknown` means all targets have unknown health status." }), + protocol: z + .enum(["http", "https", "tcp", "udp", "ssh", "rdp", "vnc"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["http", "https", "tcp", "udp", "ssh", "rdp", "vnc"], + description: + "Filter resources by protocol. `http` and `https` match HTTP resources without and with SSL respectively." + }), siteId: z.coerce.number().int().positive().optional().openapi({ type: "integer", description: @@ -403,7 +413,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -437,6 +447,7 @@ export async function listResources( enabled, query, healthStatus, + protocol, sort_by, order, siteId, @@ -632,6 +643,28 @@ export async function listResources( if (typeof healthStatus !== "undefined") { conditions.push(eq(resources.health, healthStatus)); } + + if (typeof protocol !== "undefined") { + switch (protocol) { + case "http": + conditions.push( + and( + eq(resources.mode, "http"), + eq(resources.ssl, false) + ) + ); + break; + case "https": + conditions.push( + and(eq(resources.mode, "http"), eq(resources.ssl, true)) + ); + break; + default: + conditions.push(eq(resources.mode, protocol)); + break; + } + } + if (siteId != null) { const resourcesWithSite = db .select({ resourceId: targets.resourceId }) diff --git a/server/routers/resource/listUserResourceAliases.ts b/server/routers/resource/listUserResourceAliases.ts index ae71c3708..d6e02b522 100644 --- a/server/routers/resource/listUserResourceAliases.ts +++ b/server/routers/resource/listUserResourceAliases.ts @@ -7,7 +7,7 @@ import { userOrgRoles, userOrgs } from "@server/db"; -import { and, eq, inArray, asc, isNotNull, ne } from "drizzle-orm"; +import { and, eq, inArray, asc, isNotNull, ne, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -82,7 +82,7 @@ export type ListUserResourceAliasesResponse = PaginatedResponse<{ // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), @@ -224,7 +224,7 @@ export async function listUserResourceAliases( const whereClause = and( eq(siteResources.orgId, orgId), eq(siteResources.enabled, true), - eq(siteResources.mode, "host"), + or(eq(siteResources.mode, "host"), eq(siteResources.mode, "ssh")), isNotNull(siteResources.alias), ne(siteResources.alias, ""), inArray(siteResources.siteResourceId, accessibleSiteResourceIds) diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts index 4e7dc8904..615e62438 100644 --- a/server/routers/resource/removeEmailFromResourceWhitelist.ts +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/removeRoleFromResource.ts b/server/routers/resource/removeRoleFromResource.ts index 9f1323cca..0c4d811e2 100644 --- a/server/routers/resource/removeRoleFromResource.ts +++ b/server/routers/resource/removeRoleFromResource.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/removeUserFromResource.ts b/server/routers/resource/removeUserFromResource.ts index 234ef642d..43a22c904 100644 --- a/server/routers/resource/removeUserFromResource.ts +++ b/server/routers/resource/removeUserFromResource.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index 44571eb6a..e19d47c3b 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -48,7 +48,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index 355e20a49..b52ebfe2c 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index 67651fc9e..577b11dbf 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index e091bfa85..681e00650 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -22,7 +22,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/roles", description: - "Set roles for a resource. This will replace all existing roles.", + "Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.", tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: setResourceRolesParamsSchema, @@ -40,7 +40,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index 86532c6e2..d292ccba2 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -22,7 +22,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/users", description: - "Set users for a resource. This will replace all existing users.", + "Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.", tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: setUserResourcesParamsSchema, @@ -40,7 +40,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index b228d66ed..697ae4541 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -54,7 +54,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index ea1ae66d7..77603fe9f 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -9,7 +9,11 @@ import { resourcePassword, resourcePincode, resourceRules, - resourceWhitelist + resourceWhitelist, + roleResources, + roles, + Transaction, + userResources } from "@server/db"; import { domains, @@ -62,16 +66,38 @@ const updateHttpResourceBodySchema = z .optional(), subdomain: z.string().nullable().optional(), ssl: z.boolean().optional(), - sso: z.boolean().optional(), + sso: z + .boolean() + .optional() + .describe( + "When no shared resource policy is assigned (resourcePolicyId is null), updates the resource's inline policy. When a shared policy is assigned, this value overrides the shared policy for this resource." + ), blockAccess: z.boolean().optional(), - emailWhitelistEnabled: z.boolean().optional(), - applyRules: z.boolean().optional(), + emailWhitelistEnabled: z + .boolean() + .optional() + .describe( + "When no shared resource policy is assigned (resourcePolicyId is null), updates the resource's inline policy. When a shared policy is assigned, this value overrides the shared policy for this resource." + ), + applyRules: z + .boolean() + .optional() + .describe( + "When no shared resource policy is assigned (resourcePolicyId is null), updates the resource's inline policy. When a shared policy is assigned, this value overrides the shared policy for this resource." + ), domainId: z.string().optional(), enabled: z.boolean().optional(), stickySession: z.boolean().optional(), tlsServerName: z.string().nullable().optional(), setHostHeader: z.string().nullable().optional(), - skipToIdpId: z.int().positive().nullable().optional(), + skipToIdpId: z + .int() + .positive() + .nullable() + .optional() + .describe( + "When no shared resource policy is assigned (resourcePolicyId is null), updates the resource's inline policy. When a shared policy is assigned, this value overrides the shared policy for this resource." + ), headers: z .array(z.strictObject({ name: z.string(), value: z.string() })) .nullable() @@ -87,7 +113,13 @@ const updateHttpResourceBodySchema = z pamMode: z.enum(["passthrough", "push"]).optional(), authDaemonMode: z.enum(["site", "remote", "native"]).optional(), authDaemonPort: z.int().min(1).max(65535).nullable().optional(), - resourcePolicyId: z.number().nullable().optional() + resourcePolicyId: z + .number() + .nullable() + .optional() + .describe( + "ID of the resource policy to apply to this resource. Set to null to remove the resource policy and fall back to the inline policy settings." + ) }) .refine((data) => Object.keys(data).length > 0, { error: "At least one field must be provided for update" @@ -207,7 +239,8 @@ const updateRawResourceBodySchema = z registry.registerPath({ method: "post", path: "/resource/{resourceId}", - description: "Update a resource.", + description: + "Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.", tags: [OpenAPITags.PublicResource], request: { params: updateResourceParamsSchema, @@ -227,7 +260,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -310,6 +343,61 @@ export async function updateResource( } } +async function clearResourceSpecificSettings( + resourceId: number, + orgId: string, + trx: Transaction | typeof db +) { + const adminRole = await db + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (adminRole.length === 0) { + throw new Error(`Admin role not found for org ${orgId}`); + } + // remove the resource specific pincode, password, header auth, rules, nad whitelist entries so that the resource will fall back to the policy settings + await Promise.all([ + trx + .delete(resourcePassword) + .where(eq(resourcePassword.resourceId, resourceId)), + trx + .delete(resourcePincode) + .where(eq(resourcePincode.resourceId, resourceId)), + trx + .delete(resourceHeaderAuth) + .where(eq(resourceHeaderAuth.resourceId, resourceId)), + trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resourceId + ) + ), + trx + .delete(resourceWhitelist) + .where(eq(resourceWhitelist.resourceId, resourceId)), + trx + .delete(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)), + // delete the roles and the users as well + trx + .delete(userResources) + .where(eq(userResources.resourceId, resourceId)), + // except the admin role + trx + .delete(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + ne(roleResources.roleId, adminRole[0].roleId) + ) + ) + ]); +} + async function updateHttpResource( route: { req: Request; @@ -372,6 +460,15 @@ async function updateHttpResource( } } + // catch when the resource policy changes or gets cleared + if (resource.resourcePolicyId != updateData.resourcePolicyId) { + await clearResourceSpecificSettings( + resource.resourceId, + resource.orgId, + db + ); + } + if (updateData.niceId) { const [existingResource] = await db .select() @@ -560,9 +657,17 @@ async function updateHttpResource( emailWhitelistEnabled, applyRules, skipToIdpId, - ...resourceOnlyData + ...resourceOnlyDataRest } = updateData; + const resourceOnlyData = { + ...resourceOnlyDataRest, + sso: null, // reset these because they are controlled by the inline policy + emailWhitelistEnabled: null, + applyRules: null, + skipToIdpId: null + }; + const policyUpdate: Record = {}; if (sso !== undefined) policyUpdate.sso = sso; if (emailWhitelistEnabled !== undefined) @@ -659,81 +764,6 @@ async function updateRawResource( .limit(1); await db.transaction(async (trx) => { - if (updateData.resourcePolicyId != null) { - const [existingPolicy] = await trx - .select() - .from(resourcePolicies) - .where( - eq( - resourcePolicies.resourcePolicyId, - updateData.resourcePolicyId - ) - ) - .limit(1); - - if (!existingPolicy) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource policy with ID ${updateData.resourcePolicyId} not found` - ) - ); - } - } else { - // we are in an inline policy and we need to clear out the old tables - await Promise.all([ - trx - .delete(resourcePassword) - .where( - eq( - resourcePassword.resourceId, - existingResource.resourceId - ) - ), - trx - .delete(resourcePincode) - .where( - eq( - resourcePincode.resourceId, - existingResource.resourceId - ) - ), - trx - .delete(resourceHeaderAuth) - .where( - eq( - resourceHeaderAuth.resourceId, - existingResource.resourceId - ) - ), - trx - .delete(resourceHeaderAuthExtendedCompatibility) - .where( - eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - existingResource.resourceId - ) - ), - trx - .delete(resourceWhitelist) - .where( - eq( - resourceWhitelist.resourceId, - existingResource.resourceId - ) - ), - - trx - .delete(resourceRules) - .where( - eq( - resourceRules.resourceId, - existingResource.resourceId - ) - ) - ]); - } - if (updateData.niceId) { const [existingResourceConflict] = await trx .select() @@ -758,9 +788,24 @@ async function updateRawResource( } } + await clearResourceSpecificSettings( + resource.resourceId, + resource.orgId, + trx + ); // none of these are supported on raw resources + + // we should make sure sso, emailWhitelistEnabled, and applyRules are null because this is a raw resource + const realUpdateData = { + ...updateData, + sso: null, + emailWhitelistEnabled: null, + applyRules: null, + skipToIdpId: null + }; + [updatedResource] = await trx .update(resources) - .set(updateData) + .set(realUpdateData) .where(eq(resources.resourceId, resource.resourceId)) .returning(); }); diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index 49a57ba86..cc2a6fc03 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -58,7 +58,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index e193c5018..5ad6cd435 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -62,7 +62,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index d3b56b37d..a79d11269 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -39,7 +39,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts index c90471551..8313a19c7 100644 --- a/server/routers/role/getRole.ts +++ b/server/routers/role/getRole.ts @@ -28,7 +28,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index ace5e1fc1..248db5063 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -104,7 +104,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index eb3239419..aa01899db 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -59,7 +59,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index bba487978..cc67f7b27 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -84,7 +84,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index bac56c6ba..077376211 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -33,7 +33,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index 020fbd36f..a671a47f9 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -67,7 +67,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -95,7 +95,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index f72e3c19a..d2c935571 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,20 +1,21 @@ +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { db, exitNodes, + labels, newts, orgs, remoteExitNodes, roleSites, + siteLabels, siteNetworks, siteResources, - targets, sites, + targets, userSites, - labels, - siteLabels, type Label } from "@server/db"; -import cache from "#dynamic/lib/cache"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -23,11 +24,8 @@ import type { PaginatedResponse } from "@server/types/Pagination"; import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; const listSitesParamsSchema = z.strictObject({ orgId: z.string() @@ -189,7 +187,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -236,27 +234,6 @@ export async function listSites( ); } - let accessibleSites; - if (req.user) { - accessibleSites = await db - .select({ - siteId: sql`COALESCE(${userSites.siteId}, ${roleSites.siteId})` - }) - .from(userSites) - .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId)) - .where( - or( - eq(userSites.userId, req.user!.userId), - inArray(roleSites.roleId, req.userOrgRoleIds!) - ) - ); - } else { - accessibleSites = await db - .select({ siteId: sites.siteId }) - .from(sites) - .where(eq(sites.orgId, orgId)); - } - const isLabelFeatureEnabled = await isLicensedOrSubscribed( orgId, tierMatrix.labels @@ -273,14 +250,38 @@ export async function listSites( labels: labelFilter } = parsedQuery.data; - const accessibleSiteIds = accessibleSites.map((site) => site.siteId); + const conditions = [eq(sites.orgId, orgId)]; - const conditions = [ - and( - inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId) - ) - ]; + if (req.user) { + const userAccessConditions = [ + inArray( + sites.siteId, + db + .select({ siteId: userSites.siteId }) + .from(userSites) + .where(eq(userSites.userId, req.user.userId)) + ) + ]; + + const roleIds = req.userOrgRoleIds ?? []; + if (roleIds.length > 0) { + userAccessConditions.push( + inArray( + sites.siteId, + db + .select({ siteId: roleSites.siteId }) + .from(roleSites) + .where(inArray(roleSites.roleId, roleIds)) + ) + ); + } + + conditions.push( + userAccessConditions.length === 1 + ? userAccessConditions[0] + : or(...userAccessConditions)! + ); + } if (typeof online !== "undefined") { conditions.push(eq(sites.online, online)); @@ -327,17 +328,15 @@ export async function listSites( ) ); } - conditions.push(or(...queryList)); + conditions.push(or(...queryList)!); } const baseQuery = querySitesBase().where(and(...conditions)); - // we need to add `as` so that drizzle filters the result as a subquery - const countQuery = db.$count( - querySitesBase() - .where(and(...conditions)) - .as("filtered_sites") - ); + const countQuery = db + .select({ count: sql`count(*)` }) + .from(sites) + .where(and(...conditions)); const siteListQuery = baseQuery .limit(pageSize) @@ -350,11 +349,13 @@ export async function listSites( : asc(sites.name) ); - const [totalCount, rows] = await Promise.all([ + const [countRows, rows] = await Promise.all([ countQuery, siteListQuery ]); + const totalCount = Number(countRows[0]?.count ?? 0); + const siteIds = rows.map((site) => site.siteId); let labelsForSites: Array<{ diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index 7201d2898..c6851a3c3 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -50,7 +50,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/addClientToSiteResource.ts b/server/routers/siteResource/addClientToSiteResource.ts index 03145f672..c43b755b2 100644 --- a/server/routers/siteResource/addClientToSiteResource.ts +++ b/server/routers/siteResource/addClientToSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/addRoleToSiteResource.ts b/server/routers/siteResource/addRoleToSiteResource.ts index 8f8bf27fb..a7153b3e3 100644 --- a/server/routers/siteResource/addRoleToSiteResource.ts +++ b/server/routers/siteResource/addRoleToSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/addUserToSiteResource.ts b/server/routers/siteResource/addUserToSiteResource.ts index bc6e6bcd9..6300502af 100644 --- a/server/routers/siteResource/addUserToSiteResource.ts +++ b/server/routers/siteResource/addUserToSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/batchAddClientToSiteResources.ts b/server/routers/siteResource/batchAddClientToSiteResources.ts index aad23b0d4..c8a8c90a6 100644 --- a/server/routers/siteResource/batchAddClientToSiteResources.ts +++ b/server/routers/siteResource/batchAddClientToSiteResources.ts @@ -52,7 +52,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 509ee6e1e..1eebbc01d 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -219,7 +219,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -445,7 +445,7 @@ export async function createSiteResource( let aliasAddress: string | null = null; let releaseAliasLock: (() => Promise) | null = null; - if (mode === "host" || mode === "http") { + if (mode === "host" || mode === "http" || mode === "ssh") { const { value, release } = await getNextAvailableAliasAddress(orgId); aliasAddress = value; diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 82f80b875..8ff23405c 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -33,7 +33,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index d923cb843..f25723d4b 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -21,11 +21,7 @@ const getSiteResourceParamsSchema = z.strictObject({ orgId: z.string() }); -async function query( - siteResourceId?: number, - niceId?: string, - orgId?: string -) { +async function query(siteResourceId?: number, niceId?: string, orgId?: string) { if (siteResourceId && orgId) { const [siteResource] = await db .select() @@ -75,7 +71,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -104,7 +100,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 732f2a229..5c20bc5a7 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -232,7 +232,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/listSiteResourceClients.ts b/server/routers/siteResource/listSiteResourceClients.ts index c95874b21..5ae6f6f8d 100644 --- a/server/routers/siteResource/listSiteResourceClients.ts +++ b/server/routers/siteResource/listSiteResourceClients.ts @@ -49,7 +49,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/listSiteResourceRoles.ts b/server/routers/siteResource/listSiteResourceRoles.ts index 6a451b192..d430c8c43 100644 --- a/server/routers/siteResource/listSiteResourceRoles.ts +++ b/server/routers/siteResource/listSiteResourceRoles.ts @@ -50,7 +50,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/listSiteResourceUsers.ts b/server/routers/siteResource/listSiteResourceUsers.ts index d6846b9eb..d13a58d3b 100644 --- a/server/routers/siteResource/listSiteResourceUsers.ts +++ b/server/routers/siteResource/listSiteResourceUsers.ts @@ -53,7 +53,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index 1cebb281f..311009dfa 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -69,7 +69,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/removeClientFromSiteResource.ts b/server/routers/siteResource/removeClientFromSiteResource.ts index 906f07902..35944ca15 100644 --- a/server/routers/siteResource/removeClientFromSiteResource.ts +++ b/server/routers/siteResource/removeClientFromSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/removeRoleFromSiteResource.ts b/server/routers/siteResource/removeRoleFromSiteResource.ts index 00aba1114..2759a57e7 100644 --- a/server/routers/siteResource/removeRoleFromSiteResource.ts +++ b/server/routers/siteResource/removeRoleFromSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/removeUserFromSiteResource.ts b/server/routers/siteResource/removeUserFromSiteResource.ts index 9c25ddcf2..473db41b5 100644 --- a/server/routers/siteResource/removeUserFromSiteResource.ts +++ b/server/routers/siteResource/removeUserFromSiteResource.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/setSiteResourceClients.ts b/server/routers/siteResource/setSiteResourceClients.ts index cde78d052..0f88f363f 100644 --- a/server/routers/siteResource/setSiteResourceClients.ts +++ b/server/routers/siteResource/setSiteResourceClients.ts @@ -47,7 +47,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/setSiteResourceRoles.ts b/server/routers/siteResource/setSiteResourceRoles.ts index 3e83efea2..e9878a320 100644 --- a/server/routers/siteResource/setSiteResourceRoles.ts +++ b/server/routers/siteResource/setSiteResourceRoles.ts @@ -48,7 +48,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/setSiteResourceUsers.ts b/server/routers/siteResource/setSiteResourceUsers.ts index 37e3152b7..4fa6f2218 100644 --- a/server/routers/siteResource/setSiteResourceUsers.ts +++ b/server/routers/siteResource/setSiteResourceUsers.ts @@ -48,7 +48,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index d503a2b5c..db4d4445b 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -226,7 +226,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 48ed1f5d9..2b3f472e8 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -92,7 +92,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 77614b1cd..a959e10eb 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -31,7 +31,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 37fa8b7f0..24fd6b18f 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -33,7 +33,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 1b2eb0ed5..b097b1f6e 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -103,7 +103,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index c40ffa18b..1bed7b982 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -91,7 +91,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/addUserRoleLegacy.ts b/server/routers/user/addUserRoleLegacy.ts index 201f7a7e4..bef69387a 100644 --- a/server/routers/user/addUserRoleLegacy.ts +++ b/server/routers/user/addUserRoleLegacy.ts @@ -33,7 +33,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/adminGetUser.ts b/server/routers/user/adminGetUser.ts index 1bc674339..6d5c5e664 100644 --- a/server/routers/user/adminGetUser.ts +++ b/server/routers/user/adminGetUser.ts @@ -27,7 +27,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index 8200b6f0f..f3c08f25b 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -143,7 +143,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index d39b0e57e..34732b8d8 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -87,7 +87,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/getOrgUserByUsername.ts b/server/routers/user/getOrgUserByUsername.ts index af5ad3feb..97e312b0e 100644 --- a/server/routers/user/getOrgUserByUsername.ts +++ b/server/routers/user/getOrgUserByUsername.ts @@ -46,7 +46,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index df8663d1a..ae207c26d 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -66,7 +66,10 @@ async function queryInvitations( .from(userInviteRoles) .innerJoin(roles, eq(userInviteRoles.roleId, roles.roleId)) .where( - and(eq(roles.orgId, orgId), inArray(userInviteRoles.inviteId, inviteIds)) + and( + eq(roles.orgId, orgId), + inArray(userInviteRoles.inviteId, inviteIds) + ) ); const rolesByInvite = new Map< @@ -107,7 +110,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 32e717b46..c74f48468 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -1,18 +1,21 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, idpOidcConfig } from "@server/db"; -import { - idp, - idpOrg, - roles, - userOrgRoles, - userOrgs, - users -} from "@server/db"; +import { idp, idpOrg, roles, userOrgRoles, userOrgs, users } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { and, asc, desc, eq, exists, inArray, like, or, sql } from "drizzle-orm"; +import { + and, + asc, + desc, + eq, + exists, + inArray, + like, + or, + sql +} from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -70,18 +73,23 @@ const listUsersSchema = z.strictObject({ description: "Sort order" }), idp_id: z - .preprocess((val) => { - if (val === undefined || val === null || val === "") { + .preprocess( + (val) => { + if (val === undefined || val === null || val === "") { + return undefined; + } + if (val === "internal") { + return "internal"; + } + if (typeof val === "string" && /^\d+$/.test(val)) { + return parseInt(val, 10); + } return undefined; - } - if (val === "internal") { - return "internal"; - } - if (typeof val === "string" && /^\d+$/.test(val)) { - return parseInt(val, 10); - } - return undefined; - }, z.union([z.literal("internal"), z.number().int().positive()]).optional()) + }, + z + .union([z.literal("internal"), z.number().int().positive()]) + .optional() + ) .openapi({ description: 'Filter by identity provider id, or "internal" for internal users' @@ -156,7 +164,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), @@ -203,9 +211,7 @@ export async function listUsers( const idpOk = await db .select({ one: sql`1` }) .from(idpOrg) - .where( - and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idp_id)) - ) + .where(and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idp_id))) .limit(1); if (idpOk.length === 0) { return next( diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index c8b897f71..660a900c7 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -29,7 +29,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index 982aa495d..58fc85b69 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -44,7 +44,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts index dd3ba4a9d..17fc6a659 100644 --- a/server/routers/user/updateOrgUser.ts +++ b/server/routers/user/updateOrgUser.ts @@ -43,7 +43,7 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - data: z.unknown().nullable(), + data: z.record(z.string(), z.any()).nullable(), success: z.boolean(), error: z.boolean(), message: z.string(), diff --git a/server/routers/ws/checkRoundTripMessage.ts b/server/routers/ws/checkRoundTripMessage.ts index 121f5affe..79f08d2a0 100644 --- a/server/routers/ws/checkRoundTripMessage.ts +++ b/server/routers/ws/checkRoundTripMessage.ts @@ -33,7 +33,7 @@ const checkRoundTripMessageParamsSchema = z // content: { // "application/json": { // schema: z.object({ -// data: z.unknown().nullable(), +// data: z.record(z.string(), z.any()).nullable(), // success: z.boolean(), // error: z.boolean(), // message: z.string(), @@ -84,7 +84,7 @@ export async function checkRoundTripMessage( complete: message.complete, sentAt: message.sentAt, receivedAt: message.receivedAt, - error: message.error, + error: message.error }, success: true, error: false, diff --git a/server/setup/scriptsPg/1.19.0.ts b/server/setup/scriptsPg/1.19.0.ts index 19510f68e..8d65d7807 100644 --- a/server/setup/scriptsPg/1.19.0.ts +++ b/server/setup/scriptsPg/1.19.0.ts @@ -286,6 +286,43 @@ export default async function migration() { WHERE "resources"."resourceId" = "targets"."resourceId"; `); await db.execute(sql`ALTER TABLE "targets" ADD "authToken" text;`); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD COLUMN "policyPasswordId" integer; + `); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD COLUMN "policyPincodeId" integer; + `); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD COLUMN "policyWhitelistId" integer; + `); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPasswordId_resourcePolicyPassword_passwordId_fk" FOREIGN KEY ("policyPasswordId") REFERENCES "public"."resourcePolicyPassword"("passwordId") ON DELETE cascade ON UPDATE no action; + `); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPincodeId_resourcePolicyPincode_pincodeId_fk" FOREIGN KEY ("policyPincodeId") REFERENCES "public"."resourcePolicyPincode"("pincodeId") ON DELETE cascade ON UPDATE no action; + `); + await db.execute(sql` + ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyWhitelistId_resourcePolicyWhitelist_id_fk" FOREIGN KEY ("policyWhitelistId") REFERENCES "public"."resourcePolicyWhitelist"("id") ON DELETE cascade ON UPDATE no action; + `); + // remove not null/default from sso, applyRules, and emailWhitelistEnabled in preparation for resource policies + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "sso" DROP NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "sso" DROP DEFAULT;` + ); + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "applyRules" DROP NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "applyRules" DROP DEFAULT;` + ); + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "emailWhitelistEnabled" DROP NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "resources" ALTER COLUMN "emailWhitelistEnabled" DROP DEFAULT;` + ); await db.execute(sql`COMMIT`); console.log("Migrated database"); @@ -580,26 +617,16 @@ export default async function migration() { DELETE FROM "resourceWhitelist" WHERE "resourceId" = ${resource.resourceId} `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD COLUMN "policyPasswordId" integer; - `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD COLUMN "policyPincodeId" integer; - `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD COLUMN "policyWhitelistId" integer; - `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPasswordId_resourcePolicyPassword_passwordId_fk" FOREIGN KEY ("policyPasswordId") REFERENCES "public"."resourcePolicyPassword"("passwordId") ON DELETE cascade ON UPDATE no action; - `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPincodeId_resourcePolicyPincode_pincodeId_fk" FOREIGN KEY ("policyPincodeId") REFERENCES "public"."resourcePolicyPincode"("pincodeId") ON DELETE cascade ON UPDATE no action; - `); - await db.execute(sql` - ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyWhitelistId_resourcePolicyWhitelist_id_fk" FOREIGN KEY ("policyWhitelistId") REFERENCES "public"."resourcePolicyWhitelist"("id") ON DELETE cascade ON UPDATE no action; - `); } + // clear the sso, applyRules, and emailWhitelistEnabled columns on all resources since that information is now in the resource policies + await db.execute(sql` + UPDATE "resources" + SET "sso" = null, + "applyRules" = null, + "emailWhitelistEnabled" = null + `); + await db.execute(sql`COMMIT`); console.log( `Migrated inline resource policies for ${existingResources.length} resource(s)` diff --git a/server/setup/scriptsSqlite/1.19.0.ts b/server/setup/scriptsSqlite/1.19.0.ts index dc80367d5..809340d94 100644 --- a/server/setup/scriptsSqlite/1.19.0.ts +++ b/server/setup/scriptsSqlite/1.19.0.ts @@ -680,6 +680,25 @@ export default async function migration() { deleteResourceRules.run(resource.resourceId); deleteResourceWhitelist.run(resource.resourceId); } + // remove not null/default from sso, applyRules, and emailWhitelistEnabled in preparation for resource policies + db.prepare(`ALTER TABLE 'resources' DROP COLUMN 'sso';`).run(); + db.prepare( + `ALTER TABLE 'resources' ADD COLUMN 'sso' integer;` + ).run(); + + db.prepare( + `ALTER TABLE 'resources' DROP COLUMN 'applyRules';` + ).run(); + db.prepare( + `ALTER TABLE 'resources' ADD COLUMN 'applyRules' integer;` + ).run(); + + db.prepare( + `ALTER TABLE 'resources' DROP COLUMN 'emailWhitelistEnabled';` + ).run(); + db.prepare( + `ALTER TABLE 'resources' ADD COLUMN 'emailWhitelistEnabled' integer;` + ).run(); }); migrateInlinePolicies(); diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index fe0077427..f85cbd03e 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -21,7 +21,6 @@ import { Layout } from "@app/components/Layout"; import ApplyInternalRedirect from "@app/components/ApplyInternalRedirect"; import SubscriptionViolation from "@app/components/SubscriptionViolation"; - export default async function OrgLayout(props: { children: React.ReactNode; params: Promise<{ orgId: string }>; @@ -42,6 +41,26 @@ export default async function OrgLayout(props: { redirect(`/`); } + let orgs: ListUserOrgsResponse["orgs"] = []; + try { + const getOrgs = cache(async () => + internal.get>( + `/user/${user.userId}/orgs`, + await authCookieHeader() + ) + ); + const res = await getOrgs(); + if (res && res.data.data.orgs) { + orgs = res.data.data.orgs; + } + } catch (e) {} + + const primaryOrg = orgs.find((org) => org.isPrimaryOrg); + const canViewPrimaryBilling = Boolean(primaryOrg?.isOwner); + const primaryOrgBillingHref = primaryOrg + ? `/${primaryOrg.orgId}/settings/billing` + : null; + let accessRes: CheckOrgUserAccessResponse | null = null; try { const checkOrgAccess = cache(() => @@ -58,19 +77,6 @@ export default async function OrgLayout(props: { if (!accessRes?.allowed) { // For non-admin users, show the member resources portal - let orgs: ListUserOrgsResponse["orgs"] = []; - try { - const getOrgs = cache(async () => - internal.get>( - `/user/${user.userId}/orgs`, - await authCookieHeader() - ) - ); - const res = await getOrgs(); - if (res && res.data.data.orgs) { - orgs = res.data.data.orgs; - } - } catch (e) {} return ( @@ -110,7 +116,12 @@ export default async function OrgLayout(props: { > {props.children} - {build === "saas" && } + {build === "saas" && ( + + )} diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index a2557acf9..e4abdb561 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -17,7 +17,8 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter + SettingsSectionFooter, + SettingsSectionForm } from "@app/components/Settings"; import { InfoSection, @@ -1326,44 +1327,46 @@ export default function BillingPage() { - - -
-
-
- {t("billingCurrentKeys") || - "Current Keys"} -
-
- - {getLicenseKeyCount()} - - - {getLicenseKeyCount() === 1 - ? "key" - : "keys"} - + + + +
+
+
+ {t("billingCurrentKeys") || + "Current Keys"} +
+
+ + {getLicenseKeyCount()} + + + {getLicenseKeyCount() === 1 + ? "key" + : "keys"} + +
+ +

+ {t( + "billingManageLicenseSubscriptionDescription" + ) || + "Manage your subscription for paid self-hosted license keys and download invoices."} +

- -

- {t( - "billingManageLicenseSubscriptionDescription" - ) || - "Manage your subscription for paid self-hosted license keys and download invoices."} -

-
- - + + + )} diff --git a/src/app/[orgId]/settings/clients/machine/create/page.tsx b/src/app/[orgId]/settings/clients/machine/create/page.tsx index 4ad6a1673..174555446 100644 --- a/src/app/[orgId]/settings/clients/machine/create/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/create/page.tsx @@ -14,6 +14,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -252,96 +253,102 @@ export default function Page() { -
- { - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} - id="create-client-form" - > - - - ( - - - {t("name")} - - - - - - - {t( - "clientNameDescription" - )} - - - )} - /> - - - - - {showAdvancedSettings && ( - + + + { + if (e.key === "Enter") { + e.preventDefault(); // block default enter refresh + } + }} + id="create-client-form" + > + + ( - {t( - "clientAddress" - )} + {t("name")} {t( - "addressDescription" + "clientNameDescription" )} )} /> - )} - - - + + + + {showAdvancedSettings && ( + + ( + + + {t( + "clientAddress" + )} + + + + + + + {t( + "addressDescription" + )} + + + )} + /> + + )} +
+ + +
diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index bef9b0cd7..e20523250 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -42,6 +42,7 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; +import { ExternalLink } from "lucide-react"; // Schema for general organization settings const GeneralFormSchema = z.object({ @@ -279,7 +280,16 @@ function GeneralSectionForm({ org }: SectionFormProps) { /> - {t("newtAutoUpdateDescription")} + {t("newtAutoUpdateDescription")}{" "} + + {t("learnMore")} + + diff --git a/src/app/[orgId]/settings/general/security/page.tsx b/src/app/[orgId]/settings/general/security/page.tsx index e7d0d85c8..51afa6077 100644 --- a/src/app/[orgId]/settings/general/security/page.tsx +++ b/src/app/[orgId]/settings/general/security/page.tsx @@ -224,23 +224,39 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( (option) => { - if (build != "saas") { + if ( + build != "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -249,7 +265,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -322,24 +343,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -348,7 +388,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -423,24 +468,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -449,7 +513,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -524,24 +593,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -550,7 +638,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx index e288fb1d3..574dc27cb 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx @@ -11,7 +11,6 @@ import { FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import { useResourceContext } from "@app/hooks/useResourceContext"; import DomainPicker from "@app/components/DomainPicker"; import { @@ -24,17 +23,21 @@ import { SettingsFormGrid, SettingsSectionForm, SettingsSectionHeader, - SettingsSectionTitle + SettingsSectionTitle, + SettingsSubsectionDescription, + SettingsSubsectionHeader, + SettingsSubsectionTitle } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Label } from "@app/components/ui/label"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; -import { UpdateResourceResponse } from "@server/routers/resource"; +import { + GetResourceAuthInfoResponse, + UpdateResourceResponse +} from "@server/routers/resource"; import { AxiosResponse } from "axios"; -import { AlertCircle } from "lucide-react"; import { useTranslations } from "next-intl"; import { useParams, useRouter } from "next/navigation"; import { toASCII, toUnicode } from "punycode"; @@ -44,404 +47,19 @@ import { zodResolver } from "@hookform/resolvers/zod"; import z from "zod"; import { SharedPolicySelect } from "@app/components/shared-policy-selector"; import { useOrgContext } from "@app/hooks/useOrgContext"; +import { orgQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import Link from "next/link"; import { build } from "@server/build"; import { TierFeature } from "@server/lib/billing/tierMatrix"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; -import { - Tooltip, - TooltipProvider, - TooltipTrigger -} from "@app/components/ui/tooltip"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; -import { GetResourceResponse } from "@server/routers/resource/getResource"; -import type { ResourceContextType } from "@app/contexts/resourceContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import UptimeAlertSection from "@app/components/UptimeAlertSection"; -type MaintenanceSectionFormProps = { - resource: GetResourceResponse; - updateResource: ResourceContextType["updateResource"]; -}; - -function MaintenanceSectionForm({ - resource, - updateResource -}: MaintenanceSectionFormProps) { - const { env } = useEnvContext(); - const t = useTranslations(); - const api = createApiClient({ env }); - const { isPaidUser } = usePaidStatus(); - - const MaintenanceFormSchema = z.object({ - maintenanceModeEnabled: z.boolean().optional(), - maintenanceModeType: z.enum(["forced", "automatic"]).optional(), - maintenanceTitle: z.string().max(255).optional(), - maintenanceMessage: z.string().max(2000).optional(), - maintenanceEstimatedTime: z.string().max(100).optional() - }); - - const maintenanceForm = useForm({ - resolver: zodResolver(MaintenanceFormSchema), - defaultValues: { - maintenanceModeEnabled: resource.maintenanceModeEnabled || false, - maintenanceModeType: resource.maintenanceModeType || "automatic", - maintenanceTitle: - resource.maintenanceTitle || "We'll be back soon!", - maintenanceMessage: - resource.maintenanceMessage || - "We are currently performing scheduled maintenance. Please check back soon.", - maintenanceEstimatedTime: resource.maintenanceEstimatedTime || "" - }, - mode: "onChange" - }); - - const isMaintenanceEnabled = maintenanceForm.watch( - "maintenanceModeEnabled" - ); - const maintenanceModeType = maintenanceForm.watch("maintenanceModeType"); - - const [, maintenanceFormAction, maintenanceSaveLoading] = useActionState( - onMaintenanceSubmit, - null - ); - - async function onMaintenanceSubmit() { - const isValid = await maintenanceForm.trigger(); - if (!isValid) return; - - const data = maintenanceForm.getValues(); - - const res = await api - .post>( - `resource/${resource?.resourceId}`, - { - maintenanceModeEnabled: data.maintenanceModeEnabled, - maintenanceModeType: data.maintenanceModeType, - maintenanceTitle: data.maintenanceTitle || null, - maintenanceMessage: data.maintenanceMessage || null, - maintenanceEstimatedTime: - data.maintenanceEstimatedTime || null - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorUpdate"), - description: formatAxiosError( - e, - t("resourceErrorUpdateDescription") - ) - }); - }); - - if (res && res.status === 200) { - updateResource({ - maintenanceModeEnabled: data.maintenanceModeEnabled, - maintenanceModeType: data.maintenanceModeType, - maintenanceTitle: data.maintenanceTitle || null, - maintenanceMessage: data.maintenanceMessage || null, - maintenanceEstimatedTime: data.maintenanceEstimatedTime || null - }); - - toast({ - title: t("resourceUpdated"), - description: t("resourceUpdatedDescription") - }); - } - } - - if (!["http", "ssh", "rdp", "vnc"].includes(resource.mode)) { - return null; - } - - return ( - - - - {t("maintenanceMode")} - - - {t("maintenanceModeDescription")} - - - - - - -
- - { - const isDisabled = - !isPaidUser(tierMatrix.maintencePage) || - !["http", "ssh", "rdp", "vnc"].includes( - resource.mode - ); - - return ( - -
- - - - -
- { - if ( - !isDisabled - ) { - maintenanceForm.setValue( - "maintenanceModeEnabled", - val - ); - } - }} - /> -
-
-
-
-
-
- - {t( - "enableMaintenanceModeDescription" - )} - - -
- ); - }} - /> - - {isMaintenanceEnabled && ( -
- ( - - - {t("maintenanceModeType")} - - - - - - - -
- - - {t( - "automatic" - )} - {" "} - ( - {t( - "recommended" - )} - ) - - - {t( - "automaticModeDescription" - )} - -
-
- - - - -
- - - {t( - "forced" - )} - - - - {t( - "forcedModeDescription" - )} - -
-
-
-
- -
- )} - /> - - {maintenanceModeType === "forced" && ( - - - - {t("forcedeModeWarning")} - - - )} - - ( - - - {t("pageTitle")} - - - - - - {t("pageTitleDescription")} - - - - )} - /> - - ( - - - {t( - "maintenancePageMessage" - )} - - -