diff --git a/diff b/diff new file mode 100644 index 00000000..9e71f905 --- /dev/null +++ b/diff @@ -0,0 +1,194 @@ +@@ -216,36 +216,12 @@ export const ClientResourceSchema = z.object({ + // Schema for the entire configuration object + export const ConfigSchema = z + .object({ +- "proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}), +- "client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}), +- sites: z.record(z.string(), SiteSchema).optional().default({}) ++ "proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}), ++ "client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}), ++ sites: z.record(z.string(), SiteSchema).optional().prefault({}) + }) + .refine( + // Enforce the full-domain uniqueness across resources in the same stack +- (config) => { +- // Extract all full-domain values with their resource keys +- const fullDomainMap = new Map(); +- +-- + .optional(), + log_level: z +@@ -31,14 +29,14 @@ export const configSchema = z + anonymous_usage: z.boolean().optional().default(true) + }) + .optional() +- .default({}), ++ .prefault({}), + notifications: z + .object({ + product_updates: z.boolean().optional().default(true), + new_releases: z.boolean().optional().default(true) + }) + .optional() +- .default({}) ++ .prefault({}) + }) + .optional() + .default({ +@@ -107,7 +105,7 @@ export const configSchema = z + token: z.string().optional().default("P-Access-Token") + }) + .optional() +- .default({}), ++ .prefault({}), + resource_session_request_param: z + .string() + .optional() +@@ -132,7 +130,7 @@ export const configSchema = z + credentials: z.boolean().optional() + }) + .optional(), +-- + maxmind_db_path: z.string().optional() + }) +@@ -189,7 +187,7 @@ export const configSchema = z + .default(5000) + }) + .optional() +- .default({}) ++ .prefault({}) + }) + .optional(), + traefik: z +@@ -222,7 +220,7 @@ export const configSchema = z + .default("pp-transport-v") + }) + .optional() +- .default({}), ++ .prefault({}), + gerbil: z + .object({ + exit_node_name: z.string().optional(), +@@ -247,7 +245,7 @@ export const configSchema = z + .default(30) + }) + .optional() +- .default({}), ++ .prefault({}), + orgs: z + .object({ + block_size: z.number().positive().gt(0).optional().default(24), +@@ -276,7 +274,7 @@ export const configSchema = z + .default(500) + }) + .optional() +- .default({}), ++ .prefault({}), + auth: z + .object({ + window_minutes: z +@@ -293,10 +291,10 @@ export const configSchema = z + .default(500) + }) + .optional() +- .default({}) ++ .prefault({}) + }) + .optional() +- .default({}), ++ .prefault({}), + email: z + .object({ + smtp_host: z.string().optional(), +@@ -308,7 +306,7 @@ export const configSchema = z + .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), + smtp_secure: z.boolean().optional(), + smtp_tls_reject_unauthorized: z.boolean().optional(), +-- + .optional(), + flags: z +@@ -340,7 +338,7 @@ export const configSchema = z + .default("cname.pangolin.net") + }) + .optional() +- .default({}) ++ .prefault({}) + }) + .refine( + (data) => { +@@ -355,7 +353,7 @@ export const configSchema = z + return true; + }, + { +-- + ) + .optional() +@@ -79,14 +79,14 @@ export const privateConfigSchema = z.object({ + .default("http://gerbil:3004") + }) + .optional() +- .default({}), ++ .prefault({}), + flags: z + .object({ + enable_redis: z.boolean().optional().default(false), + use_pangolin_dns: z.boolean().optional().default(false) + }) + .optional() +- .default({}), ++ .prefault({}), + branding: z + .object({ + app_name: z.string().optional(), +diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts +index 33383c25..3e0b4601 100644 +--- a/server/private/routers/auditLogs/queryAccessAuditLog.ts ++++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts +-- + .refine((val) => !isNaN(Date.parse(val)), { +- message: "timeEnd must be a valid ISO date string" ++ error: "timeEnd must be a valid ISO date string" + }) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) + .optional() +- .default(new Date().toISOString()), ++ .prefault(new Date().toISOString()), + action: z + .union([z.boolean(), z.string()]) + .transform((val) => (typeof val === "string" ? val === "true" : val)) +@@ -51,7 +51,7 @@ export const queryAccessAuditLogsQuery = z.object({ + .string() + .optional() + .transform(Number) +-- + .refine((val) => !isNaN(Date.parse(val)), { +- message: "timeEnd must be a valid ISO date string" ++ error: "timeEnd must be a valid ISO date string" + }) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) + .optional() +- .default(new Date().toISOString()), ++ .prefault(new Date().toISOString()), + action: z.string().optional(), + actorType: z.string().optional(), + actorId: z.string().optional(), +@@ -50,13 +50,13 @@ export const queryActionAuditLogsQuery = z.object({ + .optional() + .default("1000") + .transform(Number) +-- + .refine((val) => !isNaN(Date.parse(val)), { +- message: "timeEnd must be a valid ISO date string" ++ error: "timeEnd must be a valid ISO date string" + }) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) + .optional() +- .default(new Date().toISOString()), ++ .prefault(new Date().toISOString()), + action: z + .union([z.boolean(), z.string()]) + .transform((val) => (typeof val === "string" ? val === "true" : val)) +@@ -37,13 +37,13 @@ export const queryAccessAuditLogsQuery = z.object({ + .string() + .optional() + .transform(Number) diff --git a/messages/de-DE.json b/messages/de-DE.json index 001550d4..a729533d 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1021,7 +1021,7 @@ "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", - "actionApplyBlueprint": "Blaupause anwenden", + "actionApplyBlueprint": "Blueprint anwenden", "setupToken": "Setup-Token", "setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.", "setupTokenRequired": "Setup-Token ist erforderlich", @@ -1080,11 +1080,11 @@ "actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen", "actionListIdpOrgs": "IDP-Organisationen auflisten", "actionUpdateIdpOrg": "IDP-Organisation aktualisieren", - "actionCreateClient": "Kunde erstellen", - "actionDeleteClient": "Kunde löschen", - "actionUpdateClient": "Kunde aktualisieren", - "actionListClients": "Kunden auflisten", - "actionGetClient": "Kunde holen", + "actionCreateClient": "Client erstellen", + "actionDeleteClient": "Client löschen", + "actionUpdateClient": "Client aktualisieren", + "actionListClients": "Clientsn auflisten", + "actionGetClient": "Client abrufen", "actionCreateSiteResource": "Site-Ressource erstellen", "actionDeleteSiteResource": "Site-Ressource löschen", "actionGetSiteResource": "Site-Ressource abrufen", @@ -1161,7 +1161,7 @@ "sidebarAllUsers": "Alle Benutzer", "sidebarIdentityProviders": "Identitätsanbieter", "sidebarLicense": "Lizenz", - "sidebarClients": "Kunden", + "sidebarClients": "Clients", "sidebarDomains": "Domänen", "sidebarBluePrints": "Baupläne", "blueprints": "Baupläne", @@ -1175,9 +1175,9 @@ "blueprintInfo": "Blaupauseninformation", "message": "Nachricht", "blueprintContentsDescription": "Definieren Sie den YAML-Inhalt, der Ihre Infrastruktur beschreibt", - "blueprintErrorCreateDescription": "Fehler beim Anwenden der Blaupause", - "blueprintErrorCreate": "Fehler beim Erstellen der Blaupause", - "searchBlueprintProgress": "Blaupausen suchen...", + "blueprintErrorCreateDescription": "Fehler beim Anwenden des Blueprints", + "blueprintErrorCreate": "Fehler beim Erstellen des Blueprints", + "searchBlueprintProgress": "Blueprints suchen...", "appliedAt": "Angewandt am", "source": "Quelle", "contents": "Inhalt", @@ -1423,14 +1423,14 @@ }, "siteRequired": "Standort ist erforderlich.", "olmTunnel": "Olm-Tunnel", - "olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung", + "olmTunnelDescription": "Nutzen Sie Olm für die Clientverbindung", "errorCreatingClient": "Fehler beim Erstellen des Clients", "clientDefaultsNotFound": "Standardeinstellungen des Clients nicht gefunden", "createClient": "Client erstellen", "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.", "seeAllClients": "Alle Clients anzeigen", - "clientInformation": "Kundeninformationen", - "clientNamePlaceholder": "Kundenname", + "clientInformation": "Clientninformationen", + "clientNamePlaceholder": "Clientname", "address": "Adresse", "subnetPlaceholder": "Subnetz", "addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.", @@ -2049,7 +2049,7 @@ "orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt", "loadingDNSRecords": "Lade DNS-Einträge...", "olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.", - "client": "Kunde", + "client": "Client", "proxyProtocol": "Proxy-Protokoll-Einstellungen", "proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP/UDP-Dienste zu erhalten.", "enableProxyProtocol": "Proxy-Protokoll aktivieren", diff --git a/messages/en-US.json b/messages/en-US.json index 8d126e2b..39e54e0f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1279,6 +1279,15 @@ "settingsErrorUpdateDescription": "An error occurred while updating settings", "sidebarCollapse": "Collapse", "sidebarExpand": "Expand", + "productUpdateMoreInfo": "{noOfUpdates} more updates", + "productUpdateInfo": "{noOfUpdates} updates", + "productUpdateWhatsNew": "What's New", + "productUpdateTitle": "Product Updates", + "productUpdateEmpty": "No updates", + "dismissAll": "Dismiss all", + "pangolinUpdateAvailable": "New version available", + "pangolinUpdateAvailableInfo": "Version {version} is ready to install", + "pangolinUpdateAvailableReleaseNotes": "View release notes", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", @@ -1521,6 +1530,13 @@ "resourcesTableNoInternalResourcesFound": "No internal resources found.", "resourcesTableDestination": "Destination", "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "resourcesTableNoTargets": "No targets", + "resourcesTableHealthy": "Healthy", + "resourcesTableDegraded": "Degraded", + "resourcesTableOffline": "Offline", + "resourcesTableUnknown": "Unknown", + "resourcesTableNotMonitored": "Not monitored", "editInternalResourceDialogEditClientResource": "Edit Client Resource", "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", "editInternalResourceDialogResourceProperties": "Resource Properties", @@ -2155,5 +2171,31 @@ "clients": "Clients", "accessClientSelect": "Select machine clients", "resourceClientDescription": "Machine clients that can access this resource", - "regenerate": "Regenerate" + "regenerate": "Regenerate", + "credentials": "Credentials", + "savecredentials": "Save Credentials", + "regeneratecredentials": "Re-key", + "regenerateCredentials": "Regenerate and save your credentials", + "generatedcredentials": "Generated Credentials", + "copyandsavethesecredentials": "Copy and save these credentials", + "copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.", + "credentialsSaved" : "Credentials Saved", + "credentialsSavedDescription": "Credentials have been regenerated and saved successfully.", + "credentialsSaveError": "Credentials Save Error", + "credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.", + "regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.", + "confirm": "Confirm", + "regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?", + "endpoint": "Endpoint", + "Id": "Id", + "SecretKey": "Secret Key", + "featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.", + "niceId": "Nice ID", + "niceIdUpdated": "Nice ID Updated", + "niceIdUpdatedSuccessfully": "Nice ID Updated Successfully", + "niceIdUpdateError": "Error updating Nice ID", + "niceIdUpdateErrorDescription": "An error occurred while updating the Nice ID.", + "niceIdCannotBeEmpty": "Nice ID cannot be empty", + "enterIdentifier": "Enter identifier", + "identifier": "Identifier" } diff --git a/next.config.mjs b/next.config.ts similarity index 67% rename from next.config.mjs rename to next.config.ts index d771dbca..a211a701 100644 --- a/next.config.mjs +++ b/next.config.ts @@ -1,14 +1,13 @@ +import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); -/** @type {import("next").NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { eslint: { ignoreDuringBuilds: true }, - output: "standalone", - + output: "standalone" }; export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 2004db01..193007fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.3.4", + "@asteasolutions/zod-to-openapi": "8.1.0", "@aws-sdk/client-s3": "3.922.0", "@faker-js/faker": "^10.1.0", + "@headlessui/react": "^2.2.9", "@hookform/resolvers": "5.2.2", "@monaco-editor/react": "^4.7.0", "@node-rs/argon2": "^2.0.2", @@ -41,6 +42,7 @@ "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.2", "@tailwindcss/forms": "^0.5.10", + "@tanstack/react-query": "^5.90.6", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", "axios": "^1.13.1", @@ -106,14 +108,15 @@ "ws": "8.18.3", "yaml": "^2.8.1", "yargs": "18.0.0", - "zod": "3.25.76", - "zod-validation-error": "3.5.2" + "zod": "4.1.12", + "zod-validation-error": "5.0.0" }, "devDependencies": { "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", "@tailwindcss/postcss": "^4.1.17", + "@tanstack/react-query-devtools": "^5.90.2", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", @@ -172,15 +175,15 @@ } }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.4.tgz", - "integrity": "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.1.0.tgz", + "integrity": "sha512-tQFxVs05J/6QXXqIzj6rTRk3nj1HFs4pe+uThwE95jL5II2JfpVXkK+CqkO7aT0Do5AYqO6LDrKpleLUFXgY+g==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { - "zod": "^3.20.2" + "zod": "^4.0.0" } }, "node_modules/@aws-crypto/crc32": { @@ -1641,6 +1644,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -3129,6 +3133,21 @@ "@floating-ui/utils": "^0.2.10" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", @@ -3208,6 +3227,26 @@ "tslib": "2" } }, + "node_modules/@headlessui/react": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@hexagon/base64": { "version": "1.1.28", "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", @@ -4035,6 +4074,7 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -5999,6 +6039,73 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@react-aria/focus": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", + "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", + "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-email/body": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.1.0.tgz", @@ -6864,6 +6971,7 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7069,6 +7177,7 @@ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7079,6 +7188,7 @@ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -7287,6 +7397,36 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -8457,6 +8597,62 @@ "tailwindcss": "4.1.17" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.6.tgz", + "integrity": "sha512-AnZSLF26R8uX+tqb/ivdrwbVdGemdEDm1Q19qM6pry6eOZ6bEYiY7mWhzXT1YDIPTNEVcZ5kYP9nWjoxDLiIVw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", + "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.90.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", + "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.90.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.2", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", @@ -8477,6 +8673,23 @@ "react-dom": ">=16.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@tanstack/table-core": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", @@ -8490,6 +8703,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -8506,6 +8729,7 @@ "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -8592,6 +8816,7 @@ "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -8685,6 +8910,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -8720,6 +8946,7 @@ "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -8753,6 +8980,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -8763,6 +8991,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -8906,6 +9135,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -9579,6 +9809,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10108,6 +10339,7 @@ "integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -10220,6 +10452,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -11220,8 +11453,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/domutils": { "version": "3.2.2", @@ -11877,6 +12109,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -11973,6 +12206,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -12150,6 +12384,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -12276,6 +12511,18 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, + "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -12446,6 +12693,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -15055,7 +15303,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.1.7", "marked": "14.0.0" @@ -15066,7 +15313,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -15189,6 +15435,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", @@ -17635,6 +17882,7 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -18619,6 +18867,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -18795,6 +19044,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -19252,6 +19502,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -19282,6 +19533,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -19573,6 +19825,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -20066,6 +20319,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -21265,6 +21519,12 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -21279,7 +21539,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -21809,6 +22070,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22322,6 +22584,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -22624,24 +22887,25 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.2.tgz", - "integrity": "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-5.0.0.tgz", + "integrity": "sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==", "license": "MIT", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "zod": "^3.25.0" + "zod": "^3.25.0 || ^4.0.0" } } } diff --git a/package.json b/package.json index 480da7e4..d6469abb 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,10 @@ "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs" }, "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.3.4", + "@asteasolutions/zod-to-openapi": "8.1.0", "@aws-sdk/client-s3": "3.922.0", + "@faker-js/faker": "^10.1.0", + "@headlessui/react": "^2.2.9", "@hookform/resolvers": "5.2.2", "@monaco-editor/react": "^4.7.0", "@node-rs/argon2": "^2.0.2", @@ -63,6 +65,7 @@ "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.2", "@tailwindcss/forms": "^0.5.10", + "@tanstack/react-query": "^5.90.6", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", "axios": "^1.13.1", @@ -128,14 +131,14 @@ "ws": "8.18.3", "yaml": "^2.8.1", "yargs": "18.0.0", - "zod": "3.25.76", - "zod-validation-error": "3.5.2", - "@faker-js/faker": "^10.1.0" + "zod": "4.1.12", + "zod-validation-error": "5.0.0" }, "devDependencies": { "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", + "@tanstack/react-query-devtools": "^5.90.2", "@tailwindcss/postcss": "^4.1.17", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", @@ -146,9 +149,9 @@ "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/nprogress": "^0.2.3", "@types/node": "24.9.2", "@types/nodemailer": "7.0.3", + "@types/nprogress": "^0.2.3", "@types/pg": "8.15.6", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 97bee93d..71017f8d 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -19,6 +19,7 @@ export enum ActionsEnum { getSite = "getSite", listSites = "listSites", updateSite = "updateSite", + reGenerateSecret = "reGenerateSecret", createResource = "createResource", deleteResource = "deleteResource", getResource = "getResource", diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index ca3177b3..a5ee5700 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -7,20 +7,20 @@ export const SiteSchema = z.object({ export const TargetHealthCheckSchema = z.object({ hostname: z.string(), - port: z.number().int().min(1).max(65535), + port: z.int().min(1).max(65535), enabled: z.boolean().optional().default(true), path: z.string().optional(), scheme: z.string().optional(), mode: z.string().default("http"), - interval: z.number().int().default(30), - "unhealthy-interval": z.number().int().default(30), - unhealthyInterval: z.number().int().optional(), // deprecated alias - timeout: z.number().int().default(5), + interval: z.int().default(30), + "unhealthy-interval": z.int().default(30), + unhealthyInterval: z.int().optional(), // deprecated alias + timeout: z.int().default(5), headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null), "follow-redirects": z.boolean().default(true), followRedirects: z.boolean().optional(), // deprecated alias method: z.string().default("GET"), - status: z.number().int().optional() + status: z.int().optional() }); // Schema for individual target within a resource @@ -28,16 +28,16 @@ export const TargetSchema = z.object({ site: z.string().optional(), method: z.enum(["http", "https", "h2c"]).optional(), hostname: z.string(), - port: z.number().int().min(1).max(65535), + port: z.int().min(1).max(65535), enabled: z.boolean().optional().default(true), - "internal-port": z.number().int().min(1).max(65535).optional(), + "internal-port": z.int().min(1).max(65535).optional(), path: z.string().optional(), "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(), healthcheck: TargetHealthCheckSchema.optional(), rewritePath: z.string().optional(), // deprecated alias "rewrite-path": z.string().optional(), "rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), - priority: z.number().int().min(1).max(1000).optional().default(100) + priority: z.int().min(1).max(1000).optional().default(100) }); export type TargetData = z.infer; @@ -55,10 +55,10 @@ export const AuthSchema = z.object({ .optional() .default([]) .refine((roles) => !roles.includes("Admin"), { - message: "Admin role cannot be included in sso-roles" + error: "Admin role cannot be included in sso-roles" }), - "sso-users": z.array(z.string().email()).optional().default([]), - "whitelist-users": z.array(z.string().email()).optional().default([]), + "sso-users": z.array(z.email()).optional().default([]), + "whitelist-users": z.array(z.email()).optional().default([]), }); export const RuleSchema = z.object({ @@ -79,7 +79,7 @@ export const ResourceSchema = z protocol: z.enum(["http", "tcp", "udp"]).optional(), ssl: z.boolean().optional(), "full-domain": z.string().optional(), - "proxy-port": z.number().int().min(1).max(65535).optional(), + "proxy-port": z.int().min(1).max(65535).optional(), enabled: z.boolean().optional(), targets: z.array(TargetSchema.nullable()).optional().default([]), auth: AuthSchema.optional(), @@ -100,9 +100,8 @@ export const ResourceSchema = z ); }, { - message: - "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum", - path: ["name", "protocol"] + path: ["name", "protocol"], + error: "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum" } ) .refine( @@ -117,6 +116,20 @@ export const ResourceSchema = z (target) => target == null || target.method !== undefined ); } + return true; + }, + { + path: ["targets"], + error: "When protocol is 'http', all targets must have a 'method' field" + + } + ) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + // If protocol is tcp or udp, no target should have method field if (resource.protocol === "tcp" || resource.protocol === "udp") { return resource.targets.every( @@ -125,19 +138,9 @@ export const ResourceSchema = z } return true; }, - (resource) => { - if (resource.protocol === "http") { - return { - message: - "When protocol is 'http', all targets must have a 'method' field", - path: ["targets"] - }; - } - return { - message: - "When protocol is 'tcp' or 'udp', targets must not have a 'method' field", - path: ["targets"] - }; + { + path: ["targets"], + error: "When protocol is 'tcp' or 'udp', targets must not have a 'method' field" } ) .refine( @@ -156,9 +159,8 @@ export const ResourceSchema = z return true; }, { - message: - "When protocol is 'http', a 'full-domain' must be provided", - path: ["full-domain"] + path: ["full-domain"], + error: "When protocol is 'http', a 'full-domain' must be provided" } ) .refine( @@ -174,9 +176,8 @@ export const ResourceSchema = z return true; }, { - message: - "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided", - path: ["proxy-port", "exit-node"] + path: ["proxy-port", "exit-node"], + error: "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided" } ) .refine( @@ -193,9 +194,8 @@ export const ResourceSchema = z return true; }, { - message: - "When protocol is 'tcp' or 'udp', 'auth' must not be provided", - path: ["auth"] + path: ["auth"], + error: "When protocol is 'tcp' or 'udp', 'auth' must not be provided" } ); @@ -216,36 +216,12 @@ export const ClientResourceSchema = z.object({ // Schema for the entire configuration object export const ConfigSchema = z .object({ - "proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}), - "client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}), - sites: z.record(z.string(), SiteSchema).optional().default({}) + "proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}), + "client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}), + sites: z.record(z.string(), SiteSchema).optional().prefault({}) }) .refine( // Enforce the full-domain uniqueness across resources in the same stack - (config) => { - // Extract all full-domain values with their resource keys - const fullDomainMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const fullDomain = resource["full-domain"]; - if (fullDomain) { - // Only process if full-domain is defined - if (!fullDomainMap.has(fullDomain)) { - fullDomainMap.set(fullDomain, []); - } - fullDomainMap.get(fullDomain)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(fullDomainMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, (config) => { // Extract duplicates for error message const fullDomainMap = new Map(); @@ -271,38 +247,16 @@ export const ConfigSchema = z ) .join("; "); - return { - message: `Duplicate 'full-domain' values found: ${duplicates}`, - path: ["resources"] - }; + if (duplicates.length !== 0) { + return { + path: ["resources"], + error: `Duplicate 'full-domain' values found: ${duplicates}` + }; + } } ) .refine( // Enforce proxy-port uniqueness within proxy-resources per protocol - (config) => { - const protocolPortMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - const protocol = resource.protocol; - if (proxyPort !== undefined && protocol !== undefined) { - const key = `${protocol}:${proxyPort}`; - if (!protocolPortMap.has(key)) { - protocolPortMap.set(key, []); - } - protocolPortMap.get(key)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(protocolPortMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, (config) => { // Extract duplicates for error message const protocolPortMap = new Map(); @@ -331,36 +285,16 @@ export const ConfigSchema = z ) .join("; "); - return { - message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`, - path: ["proxy-resources"] - }; + if (duplicates.length !== 0) { + return { + path: ["proxy-resources"], + error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}` + }; + } } ) .refine( // Enforce proxy-port uniqueness within client-resources - (config) => { - const proxyPortMap = new Map(); - - Object.entries(config["client-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(proxyPortMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, (config) => { // Extract duplicates for error message const proxyPortMap = new Map(); @@ -385,10 +319,12 @@ export const ConfigSchema = z ) .join("; "); - return { - message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`, - path: ["client-resources"] - }; + if (duplicates.length !== 0) { + return { + path: ["client-resources"], + error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}` + }; + } } ); diff --git a/server/lib/config.ts b/server/lib/config.ts index 6cd3413e..b49814f0 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -89,6 +89,16 @@ export class Config { ? "true" : "false"; + process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app + .notifications.product_updates + ? "true" + : "false"; + + process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app + .notifications.new_releases + ? "true" + : "false"; + if (parsedConfig.server.maxmind_db_path) { process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; } @@ -158,7 +168,7 @@ export class Config { try { const response = await fetch( - "https://api.fossorial.io/api/v1/license/validate", + `https://api.fossorial.io/api/v1/license/validate`, { method: "POST", headers: { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 956d4af1..c5016777 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -308,7 +308,7 @@ export function generateRemoteSubnetsStr(allSiteResources: SiteResource[]) { if (sr.mode === "cidr") return true; if (sr.mode === "host") { // check if its a valid IP using zod - const ipSchema = z.string().ip(); + const ipSchema = z.union([z.ipv4(), z.ipv6()]); const parseResult = ipSchema.safeParse(sr.destination); return parseResult.success; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 571708ef..2da8c0a7 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -14,10 +14,8 @@ export const configSchema = z .object({ app: z .object({ - dashboard_url: z - .string() - .url() - .pipe(z.string().url()) + dashboard_url: z.url() + .pipe(z.url()) .transform((url) => url.toLowerCase()) .optional(), log_level: z @@ -31,7 +29,14 @@ export const configSchema = z anonymous_usage: z.boolean().optional().default(true) }) .optional() - .default({}) + .prefault({}), + notifications: z + .object({ + product_updates: z.boolean().optional().default(true), + new_releases: z.boolean().optional().default(true) + }) + .optional() + .prefault({}) }) .optional() .default({ @@ -40,6 +45,10 @@ export const configSchema = z log_failed_attempts: false, telemetry: { anonymous_usage: true + }, + notifications: { + product_updates: true, + new_releases: true } }), domains: z @@ -96,7 +105,7 @@ export const configSchema = z token: z.string().optional().default("P-Access-Token") }) .optional() - .default({}), + .prefault({}), resource_session_request_param: z .string() .optional() @@ -121,7 +130,7 @@ export const configSchema = z credentials: z.boolean().optional() }) .optional(), - trust_proxy: z.number().int().gte(0).optional().default(1), + trust_proxy: z.int().gte(0).optional().default(1), secret: z.string().pipe(z.string().min(8)).optional(), maxmind_db_path: z.string().optional() }) @@ -178,7 +187,7 @@ export const configSchema = z .default(5000) }) .optional() - .default({}) + .prefault({}) }) .optional(), traefik: z @@ -205,10 +214,13 @@ export const configSchema = z .default(["newt", "wireguard", "local"]), allow_raw_resources: z.boolean().optional().default(true), file_mode: z.boolean().optional().default(false), - pp_transport_prefix: z.string().optional().default("pp-transport-v") + pp_transport_prefix: z + .string() + .optional() + .default("pp-transport-v") }) .optional() - .default({}), + .prefault({}), gerbil: z .object({ exit_node_name: z.string().optional(), @@ -233,7 +245,7 @@ export const configSchema = z .default(30) }) .optional() - .default({}), + .prefault({}), orgs: z .object({ block_size: z.number().positive().gt(0).optional().default(24), @@ -262,7 +274,7 @@ export const configSchema = z .default(500) }) .optional() - .default({}), + .prefault({}), auth: z .object({ window_minutes: z @@ -279,10 +291,10 @@ export const configSchema = z .default(500) }) .optional() - .default({}) + .prefault({}) }) .optional() - .default({}), + .prefault({}), email: z .object({ smtp_host: z.string().optional(), @@ -294,7 +306,7 @@ export const configSchema = z .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), - no_reply: z.string().email().optional() + no_reply: z.email().optional() }) .optional(), flags: z @@ -315,11 +327,18 @@ export const configSchema = z nameservers: z .array(z.string().optional().optional()) .optional() - .default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]), - cname_extension: z.string().optional().default("cname.pangolin.net") + .default([ + "ns1.pangolin.net", + "ns2.pangolin.net", + "ns3.pangolin.net" + ]), + cname_extension: z + .string() + .optional() + .default("cname.pangolin.net") }) .optional() - .default({}) + .prefault({}) }) .refine( (data) => { @@ -334,7 +353,7 @@ export const configSchema = z return true; }, { - message: "At least one domain must be defined" + error: "At least one domain must be defined" } ) .refine( @@ -349,7 +368,7 @@ export const configSchema = z ); }, { - message: "Server secret must be defined" + error: "Server secret must be defined" } ) .refine( @@ -361,7 +380,7 @@ export const configSchema = z ); }, { - message: "Dashboard URL must be defined" + error: "Dashboard URL must be defined" } ); diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index f2220017..13ba1c95 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -193,7 +193,7 @@ class TelemetryClient { license_tier: licenseStatus.tier || "unknown" } }; - logger.debug("Sending enterprise startup telemtry payload:", { + logger.debug("Sending enterprise startup telemetry payload:", { payload }); // this.client.capture(payload); diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 59776105..5bdd7a14 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -1,11 +1,12 @@ import z from "zod"; +import ipaddr from "ipaddr.js"; export function isValidCIDR(cidr: string): boolean { - return z.string().cidr().safeParse(cidr).success; + return z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success; } export function isValidIP(ip: string): boolean { - return z.string().ip().safeParse(ip).success; + return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success; } export function isValidUrlGlobPattern(pattern: string): boolean { @@ -68,11 +69,11 @@ export function isUrlValid(url: string | undefined) { if (!url) return true; // the link is optional in the schema so if it's empty it's valid var pattern = new RegExp( "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", + "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name + "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address + "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path + "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string + "(\\#[-a-z\\d_]*)?$", "i" ); return !!pattern.test(url); @@ -83,12 +84,15 @@ export function isTargetValid(value: string | undefined) { const DOMAIN_REGEX = /^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/; - const IPV4_REGEX = - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; + // const IPV4_REGEX = + // /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + // const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; - if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { - return true; + try { + const addr = ipaddr.parse(value); + return addr.kind() === "ipv4" || addr.kind() === "ipv6"; + } catch { + // fall through to domain regex check } return DOMAIN_REGEX.test(value); @@ -169,10 +173,10 @@ export function isSecondLevelDomain(domain: string): boolean { } const trimmedDomain = domain.trim().toLowerCase(); - + // Split into parts const parts = trimmedDomain.split('.'); - + // Should have exactly 2 parts for a second-level domain (e.g., "example.com") if (parts.length !== 2) { return false; diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index c1981f14..754a5c53 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -50,14 +50,14 @@ export const privateConfigSchema = z.object({ host: z.string(), port: portSchema, password: z.string().optional(), - db: z.number().int().nonnegative().optional().default(0), + db: z.int().nonnegative().optional().default(0), replicas: z .array( z.object({ host: z.string(), port: portSchema, password: z.string().optional(), - db: z.number().int().nonnegative().optional().default(0) + db: z.int().nonnegative().optional().default(0) }) ) .optional() @@ -79,14 +79,14 @@ export const privateConfigSchema = z.object({ .default("http://gerbil:3004") }) .optional() - .default({}), + .prefault({}), flags: z .object({ enable_redis: z.boolean().optional().default(false), use_pangolin_dns: z.boolean().optional().default(false) }) .optional() - .default({}), + .prefault({}), branding: z .object({ app_name: z.string().optional(), diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 33383c25..3e0b4601 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -30,17 +30,17 @@ export const queryAccessAuditLogsQuery = z.object({ timeStart: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeStart must be a valid ISO date string" + error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeEnd must be a valid ISO date string" + error: "timeEnd must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .default(new Date().toISOString()), + .prefault(new Date().toISOString()), action: z .union([z.boolean(), z.string()]) .transform((val) => (typeof val === "string" ? val === "true" : val)) @@ -51,7 +51,7 @@ export const queryAccessAuditLogsQuery = z.object({ .string() .optional() .transform(Number) - .pipe(z.number().int().positive()) + .pipe(z.int().positive()) .optional(), actor: z.string().optional(), type: z.string().optional(), @@ -61,13 +61,13 @@ export const queryAccessAuditLogsQuery = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export const queryAccessAuditLogsParams = z.object({ diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 018651cb..6a5bde6d 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -30,17 +30,17 @@ export const queryActionAuditLogsQuery = z.object({ timeStart: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeStart must be a valid ISO date string" + error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeEnd must be a valid ISO date string" + error: "timeEnd must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .default(new Date().toISOString()), + .prefault(new Date().toISOString()), action: z.string().optional(), actorType: z.string().optional(), actorId: z.string().optional(), @@ -50,13 +50,13 @@ export const queryActionAuditLogsQuery = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export const queryActionAuditLogsParams = z.object({ diff --git a/server/private/routers/auth/getSessionTransferToken.ts b/server/private/routers/auth/getSessionTransferToken.ts index ba295923..bd6bc545 100644 --- a/server/private/routers/auth/getSessionTransferToken.ts +++ b/server/private/routers/auth/getSessionTransferToken.ts @@ -28,7 +28,7 @@ import { response } from "@server/lib/response"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -const paramsSchema = z.object({}).strict(); +const paramsSchema = z.strictObject({}); export type GetSessionTransferTokenRenponse = { token: string; diff --git a/server/private/routers/auth/quickStart.ts b/server/private/routers/auth/quickStart.ts index 582ac4d5..02023a0b 100644 --- a/server/private/routers/auth/quickStart.ts +++ b/server/private/routers/auth/quickStart.ts @@ -62,10 +62,10 @@ import { isTargetValid } from "@server/lib/validators"; import { listExitNodes } from "#private/lib/exitNodes"; const bodySchema = z.object({ - email: z.string().toLowerCase().email(), + email: z.email().toLowerCase(), ip: z.string().refine(isTargetValid), method: z.enum(["http", "https"]), - port: z.number().int().min(1).max(65535), + port: z.int().min(1).max(65535), pincode: z .string() .regex(/^\d{6}$/) diff --git a/server/private/routers/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSession.ts index 6e1e28c2..e0e08a20 100644 --- a/server/private/routers/billing/createCheckoutSession.ts +++ b/server/private/routers/billing/createCheckoutSession.ts @@ -25,11 +25,9 @@ import stripe from "#private/lib/stripe"; import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing"; import { getTierPriceSet, TierId } from "@server/lib/billing/tiers"; -const createCheckoutSessionSchema = z - .object({ +const createCheckoutSessionSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function createCheckoutSession( req: Request, diff --git a/server/private/routers/billing/createPortalSession.ts b/server/private/routers/billing/createPortalSession.ts index eb55f007..a3a2f04f 100644 --- a/server/private/routers/billing/createPortalSession.ts +++ b/server/private/routers/billing/createPortalSession.ts @@ -23,11 +23,9 @@ import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; import stripe from "#private/lib/stripe"; -const createPortalSessionSchema = z - .object({ +const createPortalSessionSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function createPortalSession( req: Request, diff --git a/server/private/routers/billing/getOrgSubscription.ts b/server/private/routers/billing/getOrgSubscription.ts index b97ca39f..adc4ee04 100644 --- a/server/private/routers/billing/getOrgSubscription.ts +++ b/server/private/routers/billing/getOrgSubscription.ts @@ -33,11 +33,9 @@ import { SubscriptionItem } from "@server/db"; -const getOrgSchema = z - .object({ +const getOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index bc879659..9e605cca 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -27,11 +27,9 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { GetOrgUsageResponse } from "@server/routers/billing/types"; -const getOrgSchema = z - .object({ +const getOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/private/routers/billing/internalGetOrgTier.ts b/server/private/routers/billing/internalGetOrgTier.ts index cca96243..ec114cca 100644 --- a/server/private/routers/billing/internalGetOrgTier.ts +++ b/server/private/routers/billing/internalGetOrgTier.ts @@ -21,11 +21,9 @@ import { fromZodError } from "zod-validation-error"; import { getOrgTierData } from "#private/lib/billing"; import { GetOrgTierResponse } from "@server/routers/billing/types"; -const getOrgSchema = z - .object({ +const getOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function getOrgTier( req: Request, diff --git a/server/private/routers/certificates/getCertificate.ts b/server/private/routers/certificates/getCertificate.ts index 8392cbc0..4ff8184e 100644 --- a/server/private/routers/certificates/getCertificate.ts +++ b/server/private/routers/certificates/getCertificate.ts @@ -23,13 +23,11 @@ import { fromError } from "zod-validation-error"; import { registry } from "@server/openApi"; import { GetCertificateResponse } from "@server/routers/certificates/types"; -const getCertificateSchema = z - .object({ +const getCertificateSchema = z.strictObject({ domainId: z.string(), domain: z.string().min(1).max(255), orgId: z.string() - }) - .strict(); + }); async function query(domainId: string, domain: string) { const [domainRecord] = await db diff --git a/server/private/routers/certificates/restartCertificate.ts b/server/private/routers/certificates/restartCertificate.ts index 1ad3f6a7..a6ee5460 100644 --- a/server/private/routers/certificates/restartCertificate.ts +++ b/server/private/routers/certificates/restartCertificate.ts @@ -24,12 +24,10 @@ import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const restartCertificateParamsSchema = z - .object({ - certId: z.string().transform(stoi).pipe(z.number().int().positive()), +const restartCertificateParamsSchema = z.strictObject({ + certId: z.string().transform(stoi).pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "post", @@ -41,7 +39,7 @@ registry.registerPath({ certId: z .string() .transform(stoi) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), orgId: z.string() }) }, diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts index 745af9d3..6c9cb23c 100644 --- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -23,13 +23,11 @@ import { db, domainNamespaces, resources } from "@server/db"; import { inArray } from "drizzle-orm"; import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; -const paramsSchema = z.object({}).strict(); +const paramsSchema = z.strictObject({}); -const querySchema = z - .object({ +const querySchema = z.strictObject({ subdomain: z.string() - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/private/routers/domain/listDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts index 10bcc91b..29d5d201 100644 --- a/server/private/routers/domain/listDomainNamespaces.ts +++ b/server/private/routers/domain/listDomainNamespaces.ts @@ -23,24 +23,22 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z.object({}).strict(); +const paramsSchema = z.strictObject({}); -const querySchema = z - .object({ +const querySchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function query(limit: number, offset: number) { const res = await db diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 00ad117f..eefd175c 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -23,11 +23,15 @@ import * as license from "#private/routers/license"; import * as generateLicense from "./generatedLicense"; import * as logs from "#private/routers/auditLogs"; import * as misc from "#private/routers/misc"; +import * as reKey from "#private/routers/re-key"; import { verifyOrgAccess, verifyUserHasAction, - verifyUserIsServerAdmin + verifyUserIsServerAdmin, + verifySiteAccess, + verifyClientAccess, + verifyClientsEnabled, } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import { @@ -403,3 +407,26 @@ authenticated.get( logActionAudit(ActionsEnum.exportLogs), logs.exportAccessAuditLogs ); + +authenticated.post( + "/re-key/:clientId/regenerate-client-secret", + verifyClientsEnabled, + verifyClientAccess, + verifyUserHasAction(ActionsEnum.reGenerateSecret), + reKey.reGenerateClientSecret +); + +authenticated.post( + "/re-key/:siteId/regenerate-site-secret", + verifySiteAccess, + verifyUserHasAction(ActionsEnum.reGenerateSecret), + reKey.reGenerateSiteSecret +); + +authenticated.put( + "/re-key/:orgId/reGenerate-remote-exit-node-secret", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateRemoteExitNode), + reKey.reGenerateExitNodeSecret +); diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts index 56d65a50..2c0c4420 100644 --- a/server/private/routers/generatedLicense/generateNewLicense.ts +++ b/server/private/routers/generatedLicense/generateNewLicense.ts @@ -37,7 +37,7 @@ async function createNewLicense(orgId: string, licenseData: any): Promise { const data = await response.json(); - logger.debug("Fossorial API response:", {data}); + logger.debug("Fossorial API response:", { data }); return data; } catch (error) { console.error("Error creating new license:", error); diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts index f8da1b5a..fb54c763 100644 --- a/server/private/routers/generatedLicense/listGeneratedLicenses.ts +++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts @@ -17,7 +17,10 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; import privateConfig from "#private/lib/config"; -import { GeneratedLicenseKey, ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types"; +import { + GeneratedLicenseKey, + ListGeneratedLicenseKeysResponse +} from "@server/routers/generatedLicense/types"; async function fetchLicenseKeys(orgId: string): Promise { try { diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index a8b6a174..b76cf6f9 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -78,105 +78,78 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke import semver from "semver"; // Zod schemas for request validation -const getResourceByDomainParamsSchema = z - .object({ +const getResourceByDomainParamsSchema = z.strictObject({ domain: z.string().min(1, "Domain is required") - }) - .strict(); + }); -const getUserSessionParamsSchema = z - .object({ +const getUserSessionParamsSchema = z.strictObject({ userSessionId: z.string().min(1, "User session ID is required") - }) - .strict(); + }); -const getUserOrgRoleParamsSchema = z - .object({ +const getUserOrgRoleParamsSchema = z.strictObject({ userId: z.string().min(1, "User ID is required"), orgId: z.string().min(1, "Organization ID is required") - }) - .strict(); + }); -const getRoleResourceAccessParamsSchema = z - .object({ +const getRoleResourceAccessParamsSchema = z.strictObject({ roleId: z .string() .transform(Number) .pipe( - z.number().int().positive("Role ID must be a positive integer") + z.int().positive("Role ID must be a positive integer") ), resourceId: z .string() .transform(Number) .pipe( - z - .number() - .int() + z.int() .positive("Resource ID must be a positive integer") ) - }) - .strict(); + }); -const getUserResourceAccessParamsSchema = z - .object({ +const getUserResourceAccessParamsSchema = z.strictObject({ userId: z.string().min(1, "User ID is required"), resourceId: z .string() .transform(Number) .pipe( - z - .number() - .int() + z.int() .positive("Resource ID must be a positive integer") ) - }) - .strict(); + }); -const getResourceRulesParamsSchema = z - .object({ +const getResourceRulesParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) .pipe( - z - .number() - .int() + z.int() .positive("Resource ID must be a positive integer") ) - }) - .strict(); + }); -const validateResourceSessionTokenParamsSchema = z - .object({ +const validateResourceSessionTokenParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) .pipe( - z - .number() - .int() + z.int() .positive("Resource ID must be a positive integer") ) - }) - .strict(); + }); -const validateResourceSessionTokenBodySchema = z - .object({ +const validateResourceSessionTokenBodySchema = z.strictObject({ token: z.string().min(1, "Token is required") - }) - .strict(); + }); -const validateResourceAccessTokenBodySchema = z - .object({ +const validateResourceAccessTokenBodySchema = z.strictObject({ accessTokenId: z.string().optional(), resourceId: z.number().optional(), accessToken: z.string() - }) - .strict(); + }); // Certificates by domains query validation -const getCertificatesByDomainsQuerySchema = z - .object({ +const getCertificatesByDomainsQuerySchema = z.strictObject({ // Accept domains as string or array (domains or domains[]) domains: z .union([z.array(z.string().min(1)), z.string().min(1)]) @@ -185,8 +158,7 @@ const getCertificatesByDomainsQuerySchema = z "domains[]": z .union([z.array(z.string().min(1)), z.string().min(1)]) .optional() - }) - .strict(); + }); // Type exports for request schemas export type GetResourceByDomainParams = z.infer< @@ -591,11 +563,9 @@ hybridRouter.get( } ); -const getOrgLoginPageParamsSchema = z - .object({ +const getOrgLoginPageParamsSchema = z.strictObject({ orgId: z.string().min(1) - }) - .strict(); + }); hybridRouter.get( "/org/:orgId/login-page", @@ -1217,7 +1187,7 @@ hybridRouter.post( ); const geoIpLookupParamsSchema = z.object({ - ip: z.string().ip() + ip: z.union([z.ipv4(), z.ipv6()]) }); hybridRouter.get( "/geoip/:ip", diff --git a/server/private/routers/license/activateLicense.ts b/server/private/routers/license/activateLicense.ts index f5d610aa..55b7827e 100644 --- a/server/private/routers/license/activateLicense.ts +++ b/server/private/routers/license/activateLicense.ts @@ -20,11 +20,9 @@ import license from "#private/license/license"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ licenseKey: z.string().min(1).max(255) - }) - .strict(); + }); export async function activateLicense( req: Request, diff --git a/server/private/routers/license/deleteLicenseKey.ts b/server/private/routers/license/deleteLicenseKey.ts index 93fc4ef6..6f5469fc 100644 --- a/server/private/routers/license/deleteLicenseKey.ts +++ b/server/private/routers/license/deleteLicenseKey.ts @@ -23,11 +23,9 @@ import { eq } from "drizzle-orm"; import { licenseKey } from "@server/db"; import license from "#private/license/license"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ licenseKey: z.string().min(1).max(255) - }) - .strict(); + }); export async function deleteLicenseKey( req: Request, diff --git a/server/private/routers/loginPage/createLoginPage.ts b/server/private/routers/loginPage/createLoginPage.ts index d8ef0c68..75744026 100644 --- a/server/private/routers/loginPage/createLoginPage.ts +++ b/server/private/routers/loginPage/createLoginPage.ts @@ -35,18 +35,14 @@ import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; import { CreateLoginPageResponse } from "@server/routers/loginPage/types"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ subdomain: z.string().nullable().optional(), domainId: z.string() - }) - .strict(); + }); export type CreateLoginPageBody = z.infer; diff --git a/server/private/routers/loginPage/deleteLoginPage.ts b/server/private/routers/loginPage/deleteLoginPage.ts index bf7941e7..5271ebd8 100644 --- a/server/private/routers/loginPage/deleteLoginPage.ts +++ b/server/private/routers/loginPage/deleteLoginPage.ts @@ -25,7 +25,7 @@ import { DeleteLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ orgId: z.string(), - loginPageId: z.coerce.number() + loginPageId: z.coerce.number() }) .strict(); diff --git a/server/private/routers/loginPage/getLoginPage.ts b/server/private/routers/loginPage/getLoginPage.ts index 76e20ffb..b3bde203 100644 --- a/server/private/routers/loginPage/getLoginPage.ts +++ b/server/private/routers/loginPage/getLoginPage.ts @@ -22,11 +22,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { GetLoginPageResponse } from "@server/routers/loginPage/types"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); async function query(orgId: string) { const [res] = await db diff --git a/server/private/routers/loginPage/loadLoginPage.ts b/server/private/routers/loginPage/loadLoginPage.ts index 06038201..1b10e205 100644 --- a/server/private/routers/loginPage/loadLoginPage.ts +++ b/server/private/routers/loginPage/loadLoginPage.ts @@ -23,8 +23,8 @@ import { fromError } from "zod-validation-error"; import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; const querySchema = z.object({ - resourceId: z.coerce.number().int().positive().optional(), - idpId: z.coerce.number().int().positive().optional(), + resourceId: z.coerce.number().int().positive().optional(), + idpId: z.coerce.number().int().positive().optional(), orgId: z.string().min(1).optional(), fullDomain: z.string().min(1) }); diff --git a/server/private/routers/loginPage/updateLoginPage.ts b/server/private/routers/loginPage/updateLoginPage.ts index 4f2be084..0d02b124 100644 --- a/server/private/routers/loginPage/updateLoginPage.ts +++ b/server/private/routers/loginPage/updateLoginPage.ts @@ -31,18 +31,16 @@ import { UpdateLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ orgId: z.string(), - loginPageId: z.coerce.number() + loginPageId: z.coerce.number() }) .strict(); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ subdomain: subdomainSchema.nullable().optional(), domainId: z.string().optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }) .refine( (data) => { @@ -51,7 +49,9 @@ const bodySchema = z } return true; }, - { message: "Invalid subdomain" } + { + error: "Invalid subdomain" + } ); export type UpdateLoginPageBody = z.infer; diff --git a/server/private/routers/misc/sendSupportEmail.ts b/server/private/routers/misc/sendSupportEmail.ts index fef43ef8..f1f7a919 100644 --- a/server/private/routers/misc/sendSupportEmail.ts +++ b/server/private/routers/misc/sendSupportEmail.ts @@ -22,12 +22,10 @@ import { sendEmail } from "@server/emails"; import SupportEmail from "@server/emails/templates/SupportEmail"; import config from "@server/lib/config"; -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ body: z.string().min(1), subject: z.string().min(1).max(255) - }) - .strict(); + }); export async function sendSupportEmail( req: Request, @@ -68,7 +66,7 @@ export async function sendSupportEmail( { name: req.user?.email || "Support User", to: "support@pangolin.net", - from: req.user?.email || config.getNoReplyEmail(), + from: config.getNoReplyEmail(), subject: `Support Request: ${subject}` } ); diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index 02cef526..c3ce774e 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -29,15 +29,14 @@ import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; -const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict(); +const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ name: z.string().nonempty(), clientId: z.string().nonempty(), clientSecret: z.string().nonempty(), - authUrl: z.string().url(), - tokenUrl: z.string().url(), + authUrl: z.url(), + tokenUrl: z.url(), identifierPath: z.string().nonempty(), emailPath: z.string().optional(), namePath: z.string().optional(), @@ -45,8 +44,7 @@ const bodySchema = z autoProvision: z.boolean().optional(), variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"), roleMapping: z.string().optional() - }) - .strict(); + }); // registry.registerPath({ // method: "put", diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 711d1ce3..ca0112b2 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -26,7 +26,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z .object({ orgId: z.string().optional(), // Optional; used with org idp in saas - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); diff --git a/server/private/routers/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts index 0e6689fc..3ba85412 100644 --- a/server/private/routers/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -30,7 +30,7 @@ import { GetOrgIdpResponse } from "@server/routers/orgIdp/types"; const paramsSchema = z .object({ orgId: z.string().nonempty(), - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); diff --git a/server/private/routers/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts index 0c69ff8d..646d808c 100644 --- a/server/private/routers/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -24,28 +24,24 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; -const querySchema = z - .object({ +const querySchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string().nonempty() - }) - .strict(); + }); async function query(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index c6e54240..3826f6b3 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -31,12 +31,11 @@ import { TierId } from "@server/lib/billing/tiers"; const paramsSchema = z .object({ orgId: z.string().nonempty(), - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ name: z.string().optional(), clientId: z.string().optional(), clientSecret: z.string().optional(), @@ -48,8 +47,7 @@ const bodySchema = z scopes: z.string().optional(), autoProvision: z.boolean().optional(), roleMapping: z.string().optional() - }) - .strict(); + }); export type UpdateOrgIdpResponse = { idpId: number; diff --git a/server/private/routers/re-key/index.ts b/server/private/routers/re-key/index.ts new file mode 100644 index 00000000..41a1c967 --- /dev/null +++ b/server/private/routers/re-key/index.ts @@ -0,0 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./reGenerateClientSecret"; +export * from "./reGenerateSiteSecret"; +export * from "./reGenerateExitNodeSecret"; \ No newline at end of file diff --git a/server/private/routers/re-key/reGenerateClientSecret.ts b/server/private/routers/re-key/reGenerateClientSecret.ts new file mode 100644 index 00000000..85b3f4a6 --- /dev/null +++ b/server/private/routers/re-key/reGenerateClientSecret.ts @@ -0,0 +1,139 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, olms, } from "@server/db"; +import { clients } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { hashPassword } from "@server/auth/password"; + +const reGenerateSecretParamsSchema = z.strictObject({ + clientId: z.string().transform(Number).pipe(z.int().positive()) + }); + +const reGenerateSecretBodySchema = z.strictObject({ + olmId: z.string().min(1).optional(), + secret: z.string().min(1).optional(), + + }); + +export type ReGenerateSecretBody = z.infer; + +registry.registerPath({ + method: "post", + path: "/re-key/{clientId}/regenerate-client-secret", + description: "Regenerate a client's OLM credentials by its client ID.", + tags: [OpenAPITags.Client], + request: { + params: reGenerateSecretParamsSchema, + body: { + content: { + "application/json": { + schema: reGenerateSecretBodySchema + } + } + } + }, + responses: {} +}); + + +export async function reGenerateClientSecret( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = reGenerateSecretBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { olmId, secret } = parsedBody.data; + + const parsedParams = reGenerateSecretParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { clientId } = parsedParams.data; + + let secretHash = undefined; + if (secret) { + secretHash = await hashPassword(secret); + } + + + // Fetch the client to make sure it exists and the user has access to it + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (!client) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with ID ${clientId} not found` + ) + ); + } + + const [existingOlm] = await db + .select() + .from(olms) + .where(eq(olms.clientId, clientId)) + .limit(1); + + if (existingOlm && olmId && secretHash) { + await db + .update(olms) + .set({ + olmId, + secretHash + }) + .where(eq(olms.clientId, clientId)); + } + + return response(res, { + data: existingOlm, + success: true, + error: false, + message: "Credentials regenerated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/re-key/reGenerateExitNodeSecret.ts b/server/private/routers/re-key/reGenerateExitNodeSecret.ts new file mode 100644 index 00000000..ee3a7a87 --- /dev/null +++ b/server/private/routers/re-key/reGenerateExitNodeSecret.ts @@ -0,0 +1,127 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { NextFunction, Request, Response } from "express"; +import { db, exitNodes, exitNodeOrgs, ExitNode, ExitNodeOrg } from "@server/db"; +import HttpCode from "@server/types/HttpCode"; +import { z } from "zod"; +import { remoteExitNodes } from "@server/db"; +import createHttpError from "http-errors"; +import response from "@server/lib/response"; +import { fromError } from "zod-validation-error"; +import { hashPassword } from "@server/auth/password"; +import logger from "@server/logger"; +import { and, eq } from "drizzle-orm"; +import { UpdateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; +import { OpenAPITags, registry } from "@server/openApi"; + +export const paramsSchema = z.object({ + orgId: z.string() +}); + +const bodySchema = z.strictObject({ + remoteExitNodeId: z.string().length(15), + secret: z.string().length(48) + }); + + +registry.registerPath({ + method: "post", + path: "/re-key/{orgId}/regenerate-secret", + description: "Regenerate a exit node credentials by its org ID.", + tags: [OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function reGenerateExitNodeSecret( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { remoteExitNodeId, secret } = parsedBody.data; + + if (req.user && !req.userOrgRoleId) { + return next( + createHttpError(HttpCode.FORBIDDEN, "User does not have a role") + ); + } + + const [existingRemoteExitNode] = await db + .select() + .from(remoteExitNodes) + .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); + + if (!existingRemoteExitNode) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Remote Exit Node does not exist") + ); + } + + const secretHash = await hashPassword(secret); + + await db + .update(remoteExitNodes) + .set({ secretHash }) + .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); + + return response(res, { + data: { + remoteExitNodeId, + secret, + }, + success: true, + error: false, + message: "Remote Exit Node secret updated successfully", + status: HttpCode.OK, + }); + } catch (e) { + logger.error("Failed to update remoteExitNode", e); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to update remoteExitNode" + ) + ); + } +} diff --git a/server/private/routers/re-key/reGenerateSiteSecret.ts b/server/private/routers/re-key/reGenerateSiteSecret.ts new file mode 100644 index 00000000..bfa5df9d --- /dev/null +++ b/server/private/routers/re-key/reGenerateSiteSecret.ts @@ -0,0 +1,164 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts, sites } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { hashPassword } from "@server/auth/password"; +import { addPeer } from "@server/routers/gerbil/peers"; + + +const updateSiteParamsSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); + +const updateSiteBodySchema = z.strictObject({ + type: z.enum(["newt", "wireguard"]), + newtId: z.string().min(1).max(255).optional(), + newtSecret: z.string().min(1).max(255).optional(), + exitNodeId: z.int().positive().optional(), + pubKey: z.string().optional(), + subnet: z.string().optional(), + }); + +registry.registerPath({ + method: "post", + path: "/re-key/{siteId}/regenerate-site-secret", + description: "Regenerate a site's Newt or WireGuard credentials by its site ID.", + tags: [OpenAPITags.Site], + request: { + params: updateSiteParamsSchema, + body: { + content: { + "application/json": { + schema: updateSiteBodySchema, + }, + }, + }, + }, + responses: {}, +}); + +export async function reGenerateSiteSecret( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateSiteParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError(HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString()) + ); + } + + const parsedBody = updateSiteBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError(HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString()) + ); + } + + const { siteId } = parsedParams.data; + const { type, exitNodeId, pubKey, subnet, newtId, newtSecret } = parsedBody.data; + + let updatedSite = undefined; + + if (type === "newt") { + if (!newtSecret) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "newtSecret is required for newt sites") + ); + } + + const secretHash = await hashPassword(newtSecret); + + updatedSite = await db + .update(newts) + .set({ + newtId, + secretHash, + }) + .where(eq(newts.siteId, siteId)) + .returning(); + + logger.info(`Regenerated Newt credentials for site ${siteId}`); + + } else if (type === "wireguard") { + if (!pubKey) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Public key is required for wireguard sites") + ); + } + + if (!exitNodeId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Exit node ID is required for wireguard sites" + ) + ); + } + + try { + updatedSite = await db.transaction(async (tx) => { + await addPeer(exitNodeId, { + publicKey: pubKey, + allowedIps: subnet ? [subnet] : [], + }); + const result = await tx + .update(sites) + .set({ pubKey }) + .where(eq(sites.siteId, siteId)) + .returning(); + + return result; + }); + + logger.info(`Regenerated WireGuard credentials for site ${siteId}`); + } catch (err) { + logger.error( + `Transaction failed while regenerating WireGuard secret for site ${siteId}`, + err + ); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to regenerate WireGuard credentials. Rolled back transaction." + ) + ); + } + } + + return response(res, { + data: updatedSite, + success: true, + error: false, + message: "Credentials regenerated successfully", + status: HttpCode.OK, + }); + + } catch (error) { + logger.error("Unexpected error in reGenerateSiteSecret", error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An unexpected error occurred") + ); + } +} diff --git a/server/private/routers/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts index 63209ad9..5afa82ef 100644 --- a/server/private/routers/remoteExitNode/createRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts @@ -35,12 +35,10 @@ export const paramsSchema = z.object({ orgId: z.string() }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ remoteExitNodeId: z.string().length(15), secret: z.string().length(48) - }) - .strict(); + }); export type CreateRemoteExitNodeBody = z.infer; diff --git a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts index f7b9d56c..e293f421 100644 --- a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts @@ -24,12 +24,10 @@ import { fromError } from "zod-validation-error"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string().min(1), remoteExitNodeId: z.string().min(1) - }) - .strict(); + }); export async function deleteRemoteExitNode( req: Request, diff --git a/server/private/routers/remoteExitNode/getRemoteExitNode.ts b/server/private/routers/remoteExitNode/getRemoteExitNode.ts index 2ef3fb06..c7b98297 100644 --- a/server/private/routers/remoteExitNode/getRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/getRemoteExitNode.ts @@ -23,12 +23,10 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; -const getRemoteExitNodeSchema = z - .object({ +const getRemoteExitNodeSchema = z.strictObject({ orgId: z.string().min(1), remoteExitNodeId: z.string().min(1) - }) - .strict(); + }); async function query(remoteExitNodeId: string) { const [remoteExitNode] = await db diff --git a/server/private/routers/remoteExitNode/listRemoteExitNodes.ts b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts index 1029b1e9..a13a05cd 100644 --- a/server/private/routers/remoteExitNode/listRemoteExitNodes.ts +++ b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts @@ -23,11 +23,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; -const listRemoteExitNodesParamsSchema = z - .object({ +const listRemoteExitNodesParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listRemoteExitNodesSchema = z.object({ limit: z @@ -35,13 +33,13 @@ const listRemoteExitNodesSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export function queryRemoteExitNodes(orgId: string) { diff --git a/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts index e5762f0d..bb7c89d5 100644 --- a/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts +++ b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts @@ -21,11 +21,9 @@ import { fromError } from "zod-validation-error"; import { z } from "zod"; import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function pickRemoteExitNodeDefaults( req: Request, diff --git a/server/routers/accessToken/deleteAccessToken.ts b/server/routers/accessToken/deleteAccessToken.ts index 60d8789e..5de4df9b 100644 --- a/server/routers/accessToken/deleteAccessToken.ts +++ b/server/routers/accessToken/deleteAccessToken.ts @@ -10,11 +10,9 @@ import { and, eq } from "drizzle-orm"; import { db } from "@server/db"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteAccessTokenParamsSchema = z - .object({ +const deleteAccessTokenParamsSchema = z.strictObject({ accessTokenId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "delete", diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index 631b5924..36a20268 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -24,22 +24,18 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; import { OpenAPITags, registry } from "@server/openApi"; -export const generateAccessTokenBodySchema = z - .object({ - validForSeconds: z.number().int().positive().optional(), // seconds +export const generateAccessTokenBodySchema = z.strictObject({ + validForSeconds: z.int().positive().optional(), // seconds title: z.string().optional(), description: z.string().optional() - }) - .strict(); + }); -export const generateAccssTokenParamsSchema = z - .object({ +export const generateAccssTokenParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export type GenerateAccessTokenResponse = Omit< ResourceAccessToken, diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index ab2bf826..476c858b 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -17,18 +17,16 @@ import stoi from "@server/lib/stoi"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listAccessTokensParamsSchema = z - .object({ +const listAccessTokensParamsSchema = z.strictObject({ resourceId: z .string() .optional() .transform(stoi) - .pipe(z.number().int().positive().optional()), + .pipe(z.int().positive().optional()), orgId: z.string().optional() }) - .strict() .refine((data) => !!data.resourceId !== !!data.orgId, { - message: "Either resourceId or orgId must be provided, but not both" + error: "Either resourceId or orgId must be provided, but not both" }); const listAccessTokensSchema = z.object({ @@ -37,14 +35,14 @@ const listAccessTokensSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function queryAccessTokens( diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts index 0754574a..8e9e571d 100644 --- a/server/routers/apiKeys/createRootApiKey.ts +++ b/server/routers/apiKeys/createRootApiKey.ts @@ -14,11 +14,9 @@ import { import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ name: z.string().min(1).max(255) - }) - .strict(); + }); export type CreateRootApiKeyBody = z.infer; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts index 51d20b24..7432d175 100644 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ b/server/routers/apiKeys/listApiKeyActions.ts @@ -20,13 +20,13 @@ const querySchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function queryActions(apiKeyId: string) { diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts index e8c8bc1c..53191ba6 100644 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ b/server/routers/apiKeys/listOrgApiKeys.ts @@ -16,13 +16,13 @@ const querySchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); const paramsSchema = z.object({ diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts index ddfade3c..654b830a 100644 --- a/server/routers/apiKeys/listRootApiKeys.ts +++ b/server/routers/apiKeys/listRootApiKeys.ts @@ -15,13 +15,13 @@ const querySchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function queryApiKeys() { diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts index bb16deb5..fe8cc4f1 100644 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -10,13 +10,10 @@ import { fromError } from "zod-validation-error"; import { eq, and, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const bodySchema = z - .object({ - actionIds: z - .array(z.string().nonempty()) +const bodySchema = z.strictObject({ + actionIds: z.tuple([z.string()], z.string()) .transform((v) => Array.from(new Set(v))) - }) - .strict(); + }); const paramsSchema = z.object({ apiKeyId: z.string().nonempty() diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts index f03eec18..d60aad73 100644 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ b/server/routers/apiKeys/setApiKeyOrgs.ts @@ -9,13 +9,10 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and, inArray } from "drizzle-orm"; -const bodySchema = z - .object({ - orgIds: z - .array(z.string().nonempty()) +const bodySchema = z.strictObject({ + orgIds: z.tuple([z.string()], z.string()) .transform((v) => Array.from(new Set(v))) - }) - .strict(); + }); const paramsSchema = z.object({ apiKeyId: z.string().nonempty() diff --git a/server/routers/auditLogs/queryRequstAuditLog.ts b/server/routers/auditLogs/queryRequstAuditLog.ts index 26cba417..342b7091 100644 --- a/server/routers/auditLogs/queryRequstAuditLog.ts +++ b/server/routers/auditLogs/queryRequstAuditLog.ts @@ -17,17 +17,17 @@ export const queryAccessAuditLogsQuery = z.object({ timeStart: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeStart must be a valid ISO date string" + error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { - message: "timeEnd must be a valid ISO date string" + error: "timeEnd must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .default(new Date().toISOString()), + .prefault(new Date().toISOString()), action: z .union([z.boolean(), z.string()]) .transform((val) => (typeof val === "string" ? val === "true" : val)) @@ -37,13 +37,13 @@ export const queryAccessAuditLogsQuery = z.object({ .string() .optional() .transform(Number) - .pipe(z.number().int().positive()) + .pipe(z.int().positive()) .optional(), resourceId: z .string() .optional() .transform(Number) - .pipe(z.number().int().positive()) + .pipe(z.int().positive()) .optional(), actor: z.string().optional(), location: z.string().optional(), @@ -54,13 +54,13 @@ export const queryAccessAuditLogsQuery = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export const queryRequestAuditLogsParams = z.object({ diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index 0164316e..fa007d37 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -22,13 +22,11 @@ import { sendEmail } from "@server/emails"; import ConfirmPasswordReset from "@server/emails/templates/NotifyResetPassword"; import config from "@server/lib/config"; -export const changePasswordBody = z - .object({ +export const changePasswordBody = z.strictObject({ oldPassword: z.string(), newPassword: passwordSchema, code: z.string().optional() - }) - .strict(); + }); export type ChangePasswordBody = z.infer; diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts index 9840d564..39466400 100644 --- a/server/routers/auth/checkResourceSession.ts +++ b/server/routers/auth/checkResourceSession.ts @@ -7,10 +7,10 @@ import { response } from "@server/lib/response"; import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import logger from "@server/logger"; -export const params = z.object({ +export const params = z.strictObject({ token: z.string(), - resourceId: z.string().transform(Number).pipe(z.number().int().positive()), -}).strict(); + resourceId: z.string().transform(Number).pipe(z.int().positive()), +}); export type CheckResourceSessionParams = z.infer; diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index da19c0d7..ebf6ab52 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -16,12 +16,10 @@ import config from "@server/lib/config"; import { unauthorized } from "@server/auth/unauthorizedResponse"; import { UserType } from "@server/types/UserTypes"; -export const disable2faBody = z - .object({ +export const disable2faBody = z.strictObject({ password: z.string(), code: z.string().optional() - }) - .strict(); + }); export type Disable2faBody = z.infer; diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 418eaaa4..9c913054 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -20,14 +20,12 @@ import { verifySession } from "@server/auth/sessions/verifySession"; import { UserType } from "@server/types/UserTypes"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -export const loginBodySchema = z - .object({ - email: z.string().toLowerCase().email(), +export const loginBodySchema = z.strictObject({ + email: z.email().toLowerCase(), password: z.string(), code: z.string().optional(), resourceGuid: z.string().optional() - }) - .strict(); + }); export type LoginBody = z.infer; diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index a7e84b9e..0f9953e8 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -17,11 +17,9 @@ import ResetPasswordCode from "@server/emails/templates/ResetPasswordCode"; import { hashPassword } from "@server/auth/password"; import { UserType } from "@server/types/UserTypes"; -export const requestPasswordResetBody = z - .object({ - email: z.string().toLowerCase().email() - }) - .strict(); +export const requestPasswordResetBody = z.strictObject({ + email: z.email().toLowerCase() + }); export type RequestPasswordResetBody = z.infer; diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index 7c122a44..53d80147 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -16,12 +16,10 @@ import { UserType } from "@server/types/UserTypes"; import { verifySession } from "@server/auth/sessions/verifySession"; import config from "@server/lib/config"; -export const requestTotpSecretBody = z - .object({ +export const requestTotpSecretBody = z.strictObject({ password: z.string(), - email: z.string().email().optional() - }) - .strict(); + email: z.email().optional() + }); export type RequestTotpSecretBody = z.infer; diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index 14b4236b..aeb85558 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -17,14 +17,12 @@ import ConfirmPasswordReset from "@server/emails/templates/NotifyResetPassword"; import { sendEmail } from "@server/emails"; import { passwordSchema } from "@server/auth/passwordSchema"; -export const resetPasswordBody = z - .object({ - email: z.string().toLowerCase().email(), +export const resetPasswordBody = z.strictObject({ + email: z.email().toLowerCase(), token: z.string(), // reset secret code newPassword: passwordSchema, code: z.string().optional() // 2fa code - }) - .strict(); + }); export type ResetPasswordBody = z.infer; diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index 1e75764b..cde2f61a 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -99,28 +99,28 @@ async function clearChallenge(sessionId: string) { await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); } -export const registerSecurityKeyBody = z.object({ +export const registerSecurityKeyBody = z.strictObject({ name: z.string().min(1), password: z.string().min(1), code: z.string().optional() -}).strict(); +}); -export const verifyRegistrationBody = z.object({ +export const verifyRegistrationBody = z.strictObject({ credential: z.any() -}).strict(); +}); -export const startAuthenticationBody = z.object({ - email: z.string().email().optional() -}).strict(); +export const startAuthenticationBody = z.strictObject({ + email: z.email().optional() +}); -export const verifyAuthenticationBody = z.object({ +export const verifyAuthenticationBody = z.strictObject({ credential: z.any() -}).strict(); +}); -export const deleteSecurityKeyBody = z.object({ +export const deleteSecurityKeyBody = z.strictObject({ password: z.string().min(1), code: z.string().optional() -}).strict(); +}); export async function startRegistration( req: Request, diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts index 1f63e411..9c2489cd 100644 --- a/server/routers/auth/setServerAdmin.ts +++ b/server/routers/auth/setServerAdmin.ts @@ -13,8 +13,8 @@ import { eq, and } from "drizzle-orm"; import { UserType } from "@server/types/UserTypes"; import moment from "moment"; -const bodySchema = z.object({ - email: z.string().toLowerCase().email(), +export const bodySchema = z.object({ + email: z.email().toLowerCase(), password: passwordSchema, setupToken: z.string().min(1, "Setup token is required") }); diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index e836d109..595a9b91 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -26,7 +26,7 @@ import { build } from "@server/build"; import resend, { AudienceIds, moveEmailToAudience } from "#dynamic/lib/resend"; export const signupBodySchema = z.object({ - email: z.string().toLowerCase().email(), + email: z.email().toLowerCase(), password: passwordSchema, inviteToken: z.string().optional(), inviteId: z.string().optional(), diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts index e3c29833..1a4725b6 100644 --- a/server/routers/auth/validateSetupToken.ts +++ b/server/routers/auth/validateSetupToken.ts @@ -8,11 +8,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const validateSetupTokenSchema = z - .object({ +const validateSetupTokenSchema = z.strictObject({ token: z.string().min(1, "Token is required") - }) - .strict(); + }); export type ValidateSetupTokenResponse = { valid: boolean; diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index 47a81c0a..8d31eb45 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -13,11 +13,9 @@ import logger from "@server/logger"; import { freeLimitSet, limitsService } from "@server/lib/billing"; import { build } from "@server/build"; -export const verifyEmailBody = z - .object({ +export const verifyEmailBody = z.strictObject({ code: z.string() - }) - .strict(); + }); export type VerifyEmailBody = z.infer; diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index c44c0c53..9243c9f9 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -18,13 +18,11 @@ import { generateBackupCodes } from "@server/lib/totp"; import { verifySession } from "@server/auth/sessions/verifySession"; import { unauthorized } from "@server/auth/unauthorizedResponse"; -export const verifyTotpBody = z - .object({ - email: z.string().email().optional(), +export const verifyTotpBody = z.strictObject({ + email: z.email().optional(), password: z.string().optional(), code: z.string() - }) - .strict(); + }); export type VerifyTotpBody = z.infer; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index eec83d0f..d7fe9190 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -40,10 +40,10 @@ import { logRequestAudit } from "./logRequestAudit"; import cache from "@server/lib/cache"; const verifyResourceSessionSchema = z.object({ - sessions: z.record(z.string()).optional(), - headers: z.record(z.string()).optional(), - query: z.record(z.string()).optional(), - originalRequestURL: z.string().url(), + sessions: z.record(z.string(), z.string()).optional(), + headers: z.record(z.string(), z.string()).optional(), + query: z.record(z.string(), z.string()).optional(), + originalRequestURL: z.url(), scheme: z.string(), host: z.string(), path: z.string(), @@ -60,6 +60,7 @@ type BasicUserData = { username: string; email: string | null; name: string | null; + role: string | null; }; export type VerifyUserResponse = { @@ -883,7 +884,8 @@ async function isUserAllowedToAccessResource( return { username: user.username, email: user.email, - name: user.name + name: user.name, + role: user.role }; } @@ -896,7 +898,8 @@ async function isUserAllowedToAccessResource( return { username: user.username, email: user.email, - name: user.name + name: user.name, + role: user.role }; } diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts index 6860307b..f8c9caec 100644 --- a/server/routers/blueprints/applyJSONBlueprint.ts +++ b/server/routers/blueprints/applyJSONBlueprint.ts @@ -8,17 +8,13 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint"; -const applyBlueprintSchema = z - .object({ +const applyBlueprintSchema = z.strictObject({ blueprint: z.string() - }) - .strict(); + }); -const applyBlueprintParamsSchema = z - .object({ +const applyBlueprintParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "put", diff --git a/server/routers/blueprints/getBlueprint.ts b/server/routers/blueprints/getBlueprint.ts index 3d3f7366..45c36af7 100644 --- a/server/routers/blueprints/getBlueprint.ts +++ b/server/routers/blueprints/getBlueprint.ts @@ -12,15 +12,13 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { BlueprintData } from "./types"; -const getBlueprintSchema = z - .object({ +const getBlueprintSchema = z.strictObject({ blueprintId: z .string() .transform(stoi) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); async function query(blueprintId: number, orgId: string) { // Get the client diff --git a/server/routers/blueprints/listBlueprints.ts b/server/routers/blueprints/listBlueprints.ts index 5ae8b211..315abfed 100644 --- a/server/routers/blueprints/listBlueprints.ts +++ b/server/routers/blueprints/listBlueprints.ts @@ -10,28 +10,24 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { BlueprintData } from "./types"; -const listBluePrintsParamsSchema = z - .object({ +const listBluePrintsParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const listBluePrintsSchema = z - .object({ +const listBluePrintsSchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function queryBlueprints(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 651b1119..908ea689 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -25,23 +25,19 @@ import { listExitNodes } from "#dynamic/lib/exitNodes"; import { generateId } from "@server/auth/sessions/app"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z - .object({ +const createClientParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const createClientSchema = z.strictObject({ name: z.string().min(1).max(255), olmId: z.string(), secret: z.string(), subnet: z.string(), type: z.enum(["olm"]) - }) - .strict(); + }); -export type CreateClientBody = z.infer; +export type CreateClientBody = z.infer; export type CreateClientResponse = Client; @@ -51,11 +47,11 @@ registry.registerPath({ description: "Create a new client for an organization.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { - params: paramsSchema, + params: createClientParamsSchema, body: { content: { "application/json": { - schema: bodySchema + schema: createClientSchema } } } @@ -69,7 +65,7 @@ export async function createClient( next: NextFunction ): Promise { try { - const parsedBody = bodySchema.safeParse(req.body); + const parsedBody = createClientSchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -81,7 +77,7 @@ export async function createClient( const { name, type, olmId, secret, subnet } = parsedBody.data; - const parsedParams = paramsSchema.safeParse(req.params); + const parsedParams = createClientParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index 74d2a4fc..82ddf625 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -10,11 +10,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteClientSchema = z - .object({ - clientId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const deleteClientSchema = z.strictObject({ + clientId: z.string().transform(Number).pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index d362526f..a8730faf 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -11,11 +11,9 @@ import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getClientSchema = z - .object({ - clientId: z.string().transform(stoi).pipe(z.number().int().positive()) - }) - .strict(); +const getClientSchema = z.strictObject({ + clientId: z.string().transform(stoi).pipe(z.int().positive()) + }); async function query(clientId: number) { // Get the client diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 84fed251..886cca7e 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -78,11 +78,9 @@ async function getLatestOlmVersion(): Promise { } -const listClientsParamsSchema = z - .object({ +const listClientsParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listClientsSchema = z.object({ limit: z @@ -90,13 +88,13 @@ const listClientsSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), filter: z .enum(["user", "machine"]) .optional() diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index 6f452142..3d447ecd 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -15,11 +15,9 @@ export type PickClientDefaultsResponse = { subnet: string; }; -const pickClientDefaultsSchema = z - .object({ +const pickClientDefaultsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index f0eef459..490674f5 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { Client, db, exitNodes, sites } from "@server/db"; +import { Client, db, exitNodes, olms, sites } from "@server/db"; import { clients, clientSites } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -10,17 +10,13 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const updateClientParamsSchema = z - .object({ - clientId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const updateClientParamsSchema = z.strictObject({ + clientId: z.string().transform(Number).pipe(z.int().positive()) +}); -const updateClientSchema = z - .object({ - name: z.string().min(1).max(255).optional() - }) - .strict(); +const updateClientSchema = z.strictObject({ + name: z.string().min(1).max(255).optional() +}); export type UpdateClientBody = z.infer; diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 4c2451e3..3f223bce 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -15,20 +15,16 @@ import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; import { build } from "@server/build"; import config from "@server/lib/config"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ type: z.enum(["ns", "cname", "wildcard"]), baseDomain: subdomainSchema, certResolver: z.string().optional().nullable(), preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard - }) - .strict(); + }); export type CreateDomainResponse = { diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts index 8836584b..fe4a4805 100644 --- a/server/routers/domain/deleteOrgDomain.ts +++ b/server/routers/domain/deleteOrgDomain.ts @@ -10,12 +10,10 @@ import { and, eq } from "drizzle-orm"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ domainId: z.string(), orgId: z.string() - }) - .strict(); + }); export type DeleteAccountDomainResponse = { success: boolean; diff --git a/server/routers/domain/getDNSRecords.ts b/server/routers/domain/getDNSRecords.ts index c705b4fa..239cc455 100644 --- a/server/routers/domain/getDNSRecords.ts +++ b/server/routers/domain/getDNSRecords.ts @@ -10,12 +10,10 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { getServerIp } from "@server/lib/serverIpService"; // your in-memory IP module -const getDNSRecordsSchema = z - .object({ +const getDNSRecordsSchema = z.strictObject({ domainId: z.string(), orgId: z.string() - }) - .strict(); + }); async function query(domainId: string) { const records = await db diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts index 77bd18ae..408cf37d 100644 --- a/server/routers/domain/getDomain.ts +++ b/server/routers/domain/getDomain.ts @@ -10,14 +10,12 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { domain } from "zod/v4/core/regexes"; -const getDomainSchema = z - .object({ +const getDomainSchema = z.strictObject({ domainId: z .string() .optional(), orgId: z.string().optional() - }) - .strict(); + }); async function query(domainId?: string, orgId?: string) { if (domainId) { diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index 55ea99cb..48f22c6c 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -10,28 +10,24 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listDomainsParamsSchema = z - .object({ +const listDomainsParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const listDomainsSchema = z - .object({ +const listDomainsSchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function queryDomains(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/routers/domain/restartOrgDomain.ts b/server/routers/domain/restartOrgDomain.ts index f40f2516..f2bf7c39 100644 --- a/server/routers/domain/restartOrgDomain.ts +++ b/server/routers/domain/restartOrgDomain.ts @@ -8,12 +8,10 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ domainId: z.string(), orgId: z.string() - }) - .strict(); + }); export type RestartOrgDomainResponse = { success: boolean; diff --git a/server/routers/domain/updateDomain.ts b/server/routers/domain/updateDomain.ts index c684466e..08301189 100644 --- a/server/routers/domain/updateDomain.ts +++ b/server/routers/domain/updateDomain.ts @@ -9,19 +9,15 @@ import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string(), domainId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ certResolver: z.string().optional().nullable(), preferWildcardCert: z.boolean().optional().nullable() - }) - .strict(); + }); export type UpdateDomainResponse = { domainId: string; diff --git a/server/routers/external.ts b/server/routers/external.ts index fff37aba..7c614ce2 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -181,6 +181,7 @@ authenticated.post( client.updateClient, ); + // authenticated.get( // "/site/:siteId/roles", // verifySiteAccess, @@ -194,6 +195,7 @@ authenticated.post( logActionAudit(ActionsEnum.updateSite), site.updateSite, ); + authenticated.delete( "/site/:siteId", verifySiteAccess, diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index 448b39cd..b8c947b0 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -11,19 +11,15 @@ import config from "@server/lib/config"; import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db"; -const paramsSchema = z - .object({ - idpId: z.coerce.number(), +const paramsSchema = z.strictObject({ + idpId: z.coerce.number(), orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ roleMapping: z.string().optional(), orgMapping: z.string().optional() - }) - .strict(); + }); export type CreateIdpOrgPolicyResponse = {}; diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 67357d76..2548cb04 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -12,22 +12,20 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -const paramsSchema = z.object({}).strict(); +const paramsSchema = z.strictObject({}); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ name: z.string().nonempty(), clientId: z.string().nonempty(), clientSecret: z.string().nonempty(), - authUrl: z.string().url(), - tokenUrl: z.string().url(), + authUrl: z.url(), + tokenUrl: z.url(), identifierPath: z.string().nonempty(), emailPath: z.string().optional(), namePath: z.string().optional(), scopes: z.string().nonempty(), autoProvision: z.boolean().optional() - }) - .strict(); + }); export type CreateIdpResponse = { idpId: number; diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index 58b231b7..56c0ca98 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -13,7 +13,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z .object({ orgId: z.string().optional(), // Optional; used with org idp in saas - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index 8314a6d5..c5f18282 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -10,12 +10,10 @@ import { idp, idpOrg } from "@server/db"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z - .object({ - idpId: z.coerce.number(), +const paramsSchema = z.strictObject({ + idpId: z.coerce.number(), orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "delete", diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 3c81ce0b..2db8783f 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -19,15 +19,13 @@ import { TierId } from "@server/lib/billing/tiers"; const paramsSchema = z .object({ - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ redirectUrl: z.string() - }) - .strict(); + }); const querySchema = z.object({ orgId: z.string().optional() // check what actuall calls it diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index a202f4ea..e8651c84 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -14,7 +14,7 @@ import { decrypt } from "@server/lib/crypto"; const paramsSchema = z .object({ - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index bd288837..087b52f8 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -11,25 +11,23 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.object({ - idpId: z.coerce.number() + idpId: z.coerce.number() }); -const querySchema = z - .object({ +const querySchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function query(idpId: number, limit: number, offset: number) { const res = await db diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 150b9f88..8ce2ab78 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -10,22 +10,20 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const querySchema = z - .object({ +const querySchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function query(limit: number, offset: number) { const res = await db diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index ecbc6dbd..82d3b5f2 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -10,19 +10,15 @@ import { OpenAPITags, registry } from "@server/openApi"; import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db"; -const paramsSchema = z - .object({ - idpId: z.coerce.number(), +const paramsSchema = z.strictObject({ + idpId: z.coerce.number(), orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ roleMapping: z.string().optional(), orgMapping: z.string().optional() - }) - .strict(); + }); export type UpdateIdpOrgPolicyResponse = {}; diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 53ece68e..1dbdd00a 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -14,12 +14,11 @@ import config from "@server/lib/config"; const paramsSchema = z .object({ - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ name: z.string().optional(), clientId: z.string().optional(), clientSecret: z.string().optional(), @@ -32,8 +31,7 @@ const bodySchema = z autoProvision: z.boolean().optional(), defaultRoleMapping: z.string().optional(), defaultOrgMapping: z.string().optional() - }) - .strict(); + }); export type UpdateIdpResponse = { idpId: number; diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 114f3422..f6b21ff6 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -41,7 +41,7 @@ const ensureTrailingSlash = (url: string): string => { const paramsSchema = z .object({ - idpId: z.coerce.number() + idpId: z.coerce.number() }) .strict(); @@ -52,7 +52,7 @@ const bodySchema = z.object({ }); const querySchema = z.object({ - loginPageId: z.coerce.number().optional() + loginPageId: z.coerce.number().optional() }); export type ValidateOidcUrlCallbackResponse = { diff --git a/server/routers/newt/createNewt.ts b/server/routers/newt/createNewt.ts index 3066e4ea..930c04be 100644 --- a/server/routers/newt/createNewt.ts +++ b/server/routers/newt/createNewt.ts @@ -23,12 +23,10 @@ export type CreateNewtResponse = { secret: string; }; -const createNewtSchema = z - .object({ +const createNewtSchema = z.strictObject({ newtId: z.string(), secret: z.string() - }) - .strict(); + }); export async function createNewt( req: Request, diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 4be5ae14..2985060f 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -19,7 +19,7 @@ import { generateRemoteSubnetsStr } from "@server/lib/ip"; const inputSchema = z.object({ publicKey: z.string(), - port: z.number().int().positive() + port: z.int().positive() }); type Input = z.infer; @@ -233,7 +233,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { for (const siteResource of allSiteResources) { if (siteResource.mode == "host") { // check if this is a valid ip - const ipSchema = z.string().ip(); + const ipSchema = z.union([z.ipv4(), z.ipv6()]); if (ipSchema.safeParse(siteResource.destination).success) { targets.push({ cidr: `${siteResource.destination}/32` diff --git a/server/routers/olm/createOlm.ts b/server/routers/olm/createOlm.ts new file mode 100644 index 00000000..930c04be --- /dev/null +++ b/server/routers/olm/createOlm.ts @@ -0,0 +1,104 @@ +import { NextFunction, Request, Response } from "express"; +import { db } from "@server/db"; +import { hash } from "@node-rs/argon2"; +import HttpCode from "@server/types/HttpCode"; +import { z } from "zod"; +import { newts } from "@server/db"; +import createHttpError from "http-errors"; +import response from "@server/lib/response"; +import { SqliteError } from "better-sqlite3"; +import moment from "moment"; +import { generateSessionToken } from "@server/auth/sessions/app"; +import { createNewtSession } from "@server/auth/sessions/newt"; +import { fromError } from "zod-validation-error"; +import { hashPassword } from "@server/auth/password"; + +export const createNewtBodySchema = z.object({}); + +export type CreateNewtBody = z.infer; + +export type CreateNewtResponse = { + token: string; + newtId: string; + secret: string; +}; + +const createNewtSchema = z.strictObject({ + newtId: z.string(), + secret: z.string() + }); + +export async function createNewt( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + + const parsedBody = createNewtSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { newtId, secret } = parsedBody.data; + + if (req.user && !req.userOrgRoleId) { + return next( + createHttpError(HttpCode.FORBIDDEN, "User does not have a role") + ); + } + + const secretHash = await hashPassword(secret); + + await db.insert(newts).values({ + newtId: newtId, + secretHash, + dateCreated: moment().toISOString(), + }); + + // give the newt their default permissions: + // await db.insert(newtActions).values({ + // newtId: newtId, + // actionId: ActionsEnum.createOrg, + // orgId: null, + // }); + + const token = generateSessionToken(); + await createNewtSession(token, newtId); + + return response(res, { + data: { + newtId, + secret, + token, + }, + success: true, + error: false, + message: "Newt created successfully", + status: HttpCode.OK, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A newt with that email address already exists" + ) + ); + } else { + console.error(e); + + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create newt" + ) + ); + } + } +} diff --git a/server/routers/org/checkId.ts b/server/routers/org/checkId.ts index c5d00002..2a898c30 100644 --- a/server/routers/org/checkId.ts +++ b/server/routers/org/checkId.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const getOrgSchema = z - .object({ +const getOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function checkId( req: Request, diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index d8bcb9da..e44bf021 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -27,13 +27,11 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; -const createOrgSchema = z - .object({ +const createOrgSchema = z.strictObject({ orgId: z.string(), name: z.string().min(1).max(255), subnet: z.string() - }) - .strict(); + }); registry.registerPath({ method: "put", diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 8a424e5b..0e21a8c0 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -13,11 +13,9 @@ import { sendToClient } from "#dynamic/routers/ws"; import { deletePeer } from "../gerbil/peers"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteOrgSchema = z - .object({ +const deleteOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export type DeleteOrgResponse = {}; diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 2497f9a6..38a1c6ba 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -10,11 +10,9 @@ import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getOrgSchema = z - .object({ +const getOrgSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export type GetOrgResponse = { org: Org; diff --git a/server/routers/org/getOrgOverview.ts b/server/routers/org/getOrgOverview.ts index c19b0a57..dc704d6a 100644 --- a/server/routers/org/getOrgOverview.ts +++ b/server/routers/org/getOrgOverview.ts @@ -18,11 +18,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; -const getOrgParamsSchema = z - .object({ +const getOrgParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export type GetOrgOverviewResponse = { orgName: string; diff --git a/server/routers/org/listOrgs.ts b/server/routers/org/listOrgs.ts index 07705e48..5819dc25 100644 --- a/server/routers/org/listOrgs.ts +++ b/server/routers/org/listOrgs.ts @@ -16,13 +16,13 @@ const listOrgsSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); registry.registerPath({ diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index e3c0d06f..eb500250 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -20,13 +20,13 @@ const listOrgsSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); // registry.registerPath({ diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 8ab809e4..6e7a9b35 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -15,14 +15,11 @@ import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { cache } from "@server/lib/cache"; -const updateOrgParamsSchema = z - .object({ +const updateOrgParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const updateOrgBodySchema = z - .object({ +const updateOrgBodySchema = z.strictObject({ name: z.string().min(1).max(255).optional(), requireTwoFactor: z.boolean().optional(), maxSessionLengthHours: z.number().nullable().optional(), @@ -40,9 +37,8 @@ const updateOrgBodySchema = z .min(build === "saas" ? 0 : -1) .optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); registry.registerPath({ diff --git a/server/routers/remoteExitNode/types.ts b/server/routers/remoteExitNode/types.ts index 55d0a286..ae0c2130 100644 --- a/server/routers/remoteExitNode/types.ts +++ b/server/routers/remoteExitNode/types.ts @@ -6,6 +6,11 @@ export type CreateRemoteExitNodeResponse = { secret: string; }; +export type UpdateRemoteExitNodeResponse = { + remoteExitNodeId: string; + secret: string; +} + export type PickRemoteExitNodeDefaultsResponse = { remoteExitNodeId: string; secret: string; diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts index c0d80468..f9cee838 100644 --- a/server/routers/resource/addEmailToResourceWhitelist.ts +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -10,29 +10,22 @@ import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const addEmailToResourceWhitelistBodySchema = z - .object({ - email: z - .string() - .email() +const addEmailToResourceWhitelistBodySchema = z.strictObject({ + email: z.email() .or( z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - message: - "Invalid email address. Wildcard (*) must be the entire local part." + error: "Invalid email address. Wildcard (*) must be the entire local part." }) ) .transform((v) => v.toLowerCase()) - }) - .strict(); + }); -const addEmailToResourceWhitelistParamsSchema = z - .object({ +const addEmailToResourceWhitelistParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index bf0a9697..81ca7fbc 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -15,22 +15,18 @@ import config from "@server/lib/config"; import stoi from "@server/lib/stoi"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -const authWithAccessTokenBodySchema = z - .object({ +const authWithAccessTokenBodySchema = z.strictObject({ accessToken: z.string(), accessTokenId: z.string().optional() - }) - .strict(); + }); -const authWithAccessTokenParamsSchema = z - .object({ +const authWithAccessTokenParamsSchema = z.strictObject({ resourceId: z .string() .optional() .transform(stoi) - .pipe(z.number().int().positive().optional()) - }) - .strict(); + .pipe(z.int().positive().optional()) + }); export type AuthWithAccessTokenResponse = { session?: string; diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index 97daea3b..4c1f2058 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -15,20 +15,16 @@ import { verifyPassword } from "@server/auth/password"; import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -export const authWithPasswordBodySchema = z - .object({ +export const authWithPasswordBodySchema = z.strictObject({ password: z.string() - }) - .strict(); + }); -export const authWithPasswordParamsSchema = z - .object({ +export const authWithPasswordParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export type AuthWithPasswordResponse = { session?: string; diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 8ce5c1fe..59f80ee0 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -14,20 +14,16 @@ import { verifyPassword } from "@server/auth/password"; import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -export const authWithPincodeBodySchema = z - .object({ +export const authWithPincodeBodySchema = z.strictObject({ pincode: z.string() - }) - .strict(); + }); -export const authWithPincodeParamsSchema = z - .object({ +export const authWithPincodeParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export type AuthWithPincodeResponse = { session?: string; diff --git a/server/routers/resource/authWithWhitelist.ts b/server/routers/resource/authWithWhitelist.ts index 11e417b6..11f84043 100644 --- a/server/routers/resource/authWithWhitelist.ts +++ b/server/routers/resource/authWithWhitelist.ts @@ -14,21 +14,17 @@ import logger from "@server/logger"; import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -const authWithWhitelistBodySchema = z - .object({ - email: z.string().toLowerCase().email(), +const authWithWhitelistBodySchema = z.strictObject({ + email: z.email().toLowerCase(), otp: z.string().optional() - }) - .strict(); + }); -const authWithWhitelistParamsSchema = z - .object({ +const authWithWhitelistParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export type AuthWithWhitelistResponse = { otpSent?: boolean; diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 2a4e67a7..b9ab3ce5 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -25,14 +25,11 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; -const createResourceParamsSchema = z - .object({ +const createResourceParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const createHttpResourceSchema = z - .object({ +const createHttpResourceSchema = z.strictObject({ name: z.string().min(1).max(255), subdomain: z.string().nullable().optional(), http: z.boolean(), @@ -40,7 +37,6 @@ const createHttpResourceSchema = z domainId: z.string(), stickySession: z.boolean().optional(), }) - .strict() .refine( (data) => { if (data.subdomain) { @@ -48,18 +44,18 @@ const createHttpResourceSchema = z } return true; }, - { message: "Invalid subdomain" } + { + error: "Invalid subdomain" + } ); -const createRawResourceSchema = z - .object({ +const createRawResourceSchema = z.strictObject({ name: z.string().min(1).max(255), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.int().min(1).max(65535) // enableProxy: z.boolean().default(true) // always true now }) - .strict() .refine( (data) => { if (!config.getRawConfig().flags?.allow_raw_resources) { @@ -70,7 +66,7 @@ const createRawResourceSchema = z return true; }, { - message: "Raw resources are not allowed" + error: "Raw resources are not allowed" } ); diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index 1a5c07c2..c3e086b0 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -15,24 +15,20 @@ import { } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; -const createResourceRuleSchema = z - .object({ +const createResourceRuleSchema = z.strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]), match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]), value: z.string().min(1), - priority: z.number().int(), + priority: z.int(), enabled: z.boolean().optional() - }) - .strict(); + }); -const createResourceRuleParamsSchema = z - .object({ +const createResourceRuleParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "put", diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index 3b0e9df4..a81208a5 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -14,14 +14,12 @@ import { getAllowedIps } from "../target/helpers"; import { OpenAPITags, registry } from "@server/openApi"; // Define Zod schema for request parameters validation -const deleteResourceSchema = z - .object({ +const deleteResourceSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts index 6b404651..58cb7b48 100644 --- a/server/routers/resource/deleteResourceRule.ts +++ b/server/routers/resource/deleteResourceRule.ts @@ -10,15 +10,13 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteResourceRuleSchema = z - .object({ - ruleId: z.string().transform(Number).pipe(z.number().int().positive()), +const deleteResourceRuleSchema = z.strictObject({ + ruleId: z.string().transform(Number).pipe(z.int().positive()), resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/resource/getExchangeToken.ts b/server/routers/resource/getExchangeToken.ts index 28975234..8a0276a0 100644 --- a/server/routers/resource/getExchangeToken.ts +++ b/server/routers/resource/getExchangeToken.ts @@ -16,14 +16,12 @@ import { response } from "@server/lib/response"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; -const getExchangeTokenParams = z - .object({ +const getExchangeTokenParams = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export type GetExchangeTokenResponse = { requestToken: string; diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 0fdcdd0c..f2ce559e 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -11,18 +11,16 @@ import logger from "@server/logger"; import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; -const getResourceSchema = z - .object({ +const getResourceSchema = z.strictObject({ resourceId: z .string() .optional() .transform(stoi) - .pipe(z.number().int().positive().optional()) + .pipe(z.int().positive().optional()) .optional(), niceId: z.string().optional(), orgId: z.string().optional() - }) - .strict(); + }); async function query(resourceId?: number, niceId?: string, orgId?: string) { if (resourceId) { diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 834da7b3..60f8e586 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -15,11 +15,9 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { build } from "@server/build"; -const getResourceAuthInfoSchema = z - .object({ +const getResourceAuthInfoSchema = z.strictObject({ resourceGuid: z.string() - }) - .strict(); + }); export type GetResourceAuthInfoResponse = { resourceId: number; diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 415cb714..3171352a 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -10,14 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getResourceWhitelistSchema = z - .object({ +const getResourceWhitelistSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); async function queryWhitelist(resourceId: number) { return await db diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index 4676b01e..3dbb8c0d 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -10,14 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listResourceRolesSchema = z - .object({ +const listResourceRolesSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); async function query(resourceId: number) { return await db diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts index 727d50ba..bc2516a0 100644 --- a/server/routers/resource/listResourceRules.ts +++ b/server/routers/resource/listResourceRules.ts @@ -10,14 +10,12 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const listResourceRulesParamsSchema = z - .object({ +const listResourceRulesParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); const listResourceRulesSchema = z.object({ limit: z @@ -25,13 +23,13 @@ const listResourceRulesSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function queryResourceRules(resourceId: number) { diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index 0d96ac0d..b07bcf0a 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -10,14 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listResourceUsersSchema = z - .object({ +const listResourceUsersSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); async function queryUsers(resourceId: number) { return await db diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index e612d5ec..a72dd763 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -20,11 +20,9 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { warn } from "console"; -const listResourcesParamsSchema = z - .object({ +const listResourcesParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listResourcesSchema = z.object({ limit: z @@ -32,14 +30,14 @@ const listResourcesSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); // (resource fields + a single joined target) diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts index 7667bf28..c2cac2de 100644 --- a/server/routers/resource/removeEmailFromResourceWhitelist.ts +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -10,29 +10,22 @@ import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const removeEmailFromResourceWhitelistBodySchema = z - .object({ - email: z - .string() - .email() +const removeEmailFromResourceWhitelistBodySchema = z.strictObject({ + email: z.email() .or( z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - message: - "Invalid email address. Wildcard (*) must be the entire local part." + error: "Invalid email address. Wildcard (*) must be the entire local part." }) ) .transform((v) => v.toLowerCase()) - }) - .strict(); + }); -const removeEmailFromResourceWhitelistParamsSchema = z - .object({ +const removeEmailFromResourceWhitelistParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index dc0d417d..87ffbacd 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -11,15 +11,13 @@ import { hashPassword } from "@server/auth/password"; import { OpenAPITags, registry } from "@server/openApi"; const setResourceAuthMethodsParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) + resourceId: z.string().transform(Number).pipe(z.int().positive()) }); -const setResourceAuthMethodsBodySchema = z - .object({ +const setResourceAuthMethodsBodySchema = z.strictObject({ user: z.string().min(4).max(100).nullable(), password: z.string().min(4).max(100).nullable() - }) - .strict(); + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index 5ff485d2..3f9ce9f1 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -13,14 +13,12 @@ import { hashPassword } from "@server/auth/password"; import { OpenAPITags, registry } from "@server/openApi"; const setResourceAuthMethodsParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) + resourceId: z.string().transform(Number).pipe(z.int().positive()) }); -const setResourceAuthMethodsBodySchema = z - .object({ +const setResourceAuthMethodsBodySchema = z.strictObject({ password: z.string().min(4).max(100).nullable() - }) - .strict(); + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index 83af3c7a..6a88a279 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -14,17 +14,15 @@ import { hashPassword } from "@server/auth/password"; import { OpenAPITags, registry } from "@server/openApi"; const setResourceAuthMethodsParamsSchema = z.object({ - resourceId: z.string().transform(Number).pipe(z.number().int().positive()) + resourceId: z.string().transform(Number).pipe(z.int().positive()) }); -const setResourceAuthMethodsBodySchema = z - .object({ +const setResourceAuthMethodsBodySchema = z.strictObject({ pincode: z .string() .regex(/^\d{6}$/) .or(z.null()) - }) - .strict(); + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index 380aad74..5064c7e0 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -10,20 +10,16 @@ import { fromError } from "zod-validation-error"; import { eq, and, ne, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const setResourceRolesBodySchema = z - .object({ - roleIds: z.array(z.number().int().positive()) - }) - .strict(); +const setResourceRolesBodySchema = z.strictObject({ + roleIds: z.array(z.int().positive()) + }); -const setResourceRolesParamsSchema = z - .object({ +const setResourceRolesParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index 152c0f88..b5eca17c 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -10,20 +10,16 @@ import { fromError } from "zod-validation-error"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const setUserResourcesBodySchema = z - .object({ +const setUserResourcesBodySchema = z.strictObject({ userIds: z.array(z.string()) - }) - .strict(); + }); -const setUserResourcesParamsSchema = z - .object({ +const setUserResourcesParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 16c9150b..417ef6d9 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -10,33 +10,26 @@ import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const setResourceWhitelistBodySchema = z - .object({ +const setResourceWhitelistBodySchema = z.strictObject({ emails: z .array( - z - .string() - .email() + z.email() .or( z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - message: - "Invalid email address. Wildcard (*) must be the entire local part." + error: "Invalid email address. Wildcard (*) must be the entire local part." }) ) ) .max(50) .transform((v) => v.map((e) => e.toLowerCase())) - }) - .strict(); + }); -const setResourceWhitelistParamsSchema = z - .object({ +const setResourceWhitelistParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); registry.registerPath({ method: "post", diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 13c5220d..1008bac9 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -25,18 +25,16 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateHeaders } from "@server/lib/validators"; import { build } from "@server/build"; -const updateResourceParamsSchema = z - .object({ +const updateResourceParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); -const updateHttpResourceBodySchema = z - .object({ +const updateHttpResourceBodySchema = z.strictObject({ name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional(), subdomain: subdomainSchema.nullable().optional(), ssl: z.boolean().optional(), sso: z.boolean().optional(), @@ -48,15 +46,14 @@ const updateHttpResourceBodySchema = z stickySession: z.boolean().optional(), tlsServerName: z.string().nullable().optional(), setHostHeader: z.string().nullable().optional(), - skipToIdpId: z.number().int().positive().nullable().optional(), + skipToIdpId: z.int().positive().nullable().optional(), headers: z - .array(z.object({ name: z.string(), value: z.string() })) + .array(z.strictObject({ name: z.string(), value: z.string() })) .nullable() .optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }) .refine( (data) => { @@ -65,7 +62,9 @@ const updateHttpResourceBodySchema = z } return true; }, - { message: "Invalid subdomain" } + { + error: "Invalid subdomain" + } ) .refine( (data) => { @@ -75,8 +74,7 @@ const updateHttpResourceBodySchema = z return true; }, { - message: - "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." + error: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." } ) .refine( @@ -87,25 +85,23 @@ const updateHttpResourceBodySchema = z return true; }, { - message: - "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." + error: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } ); export type UpdateResourceResponse = Resource; -const updateRawResourceBodySchema = z - .object({ +const updateRawResourceBodySchema = z.strictObject({ name: z.string().min(1).max(255).optional(), - proxyPort: z.number().int().min(1).max(65535).optional(), + niceId: z.string().min(1).max(255).optional(), + proxyPort: z.int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), enabled: z.boolean().optional(), proxyProtocol: z.boolean().optional(), - proxyProtocolVersion: z.number().int().min(1).optional() + proxyProtocolVersion: z.int().min(1).optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }) .refine( (data) => { @@ -116,7 +112,9 @@ const updateRawResourceBodySchema = z } return true; }, - { message: "Cannot update proxyPort" } + { + error: "Cannot update proxyPort" + } ); registry.registerPath({ @@ -236,6 +234,30 @@ async function updateHttpResource( const updateData = parsedBody.data; + if (updateData.niceId) { + const [existingResource] = await db + .select() + .from(resources) + .where( + and( + eq(resources.niceId, updateData.niceId), + eq(resources.orgId, resource.orgId) + ) + ); + + if ( + existingResource && + existingResource.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A resource with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + if (updateData.domainId) { const domainId = updateData.domainId; @@ -362,6 +384,30 @@ async function updateRawResource( const updateData = parsedBody.data; + if (updateData.niceId) { + const [existingResource] = await db + .select() + .from(resources) + .where( + and( + eq(resources.niceId, updateData.niceId), + eq(resources.orgId, resource.orgId) + ) + ); + + if ( + existingResource && + existingResource.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A resource with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + const updatedResource = await db .update(resources) .set(updateData) diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index 8df70c0f..b92c3d07 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -16,28 +16,24 @@ import { import { OpenAPITags, registry } from "@server/openApi"; // Define Zod schema for request parameters validation -const updateResourceRuleParamsSchema = z - .object({ - ruleId: z.string().transform(Number).pipe(z.number().int().positive()), +const updateResourceRuleParamsSchema = z.strictObject({ + ruleId: z.string().transform(Number).pipe(z.int().positive()), resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); // Define Zod schema for request body validation -const updateResourceRuleSchema = z - .object({ +const updateResourceRuleSchema = z.strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(), value: z.string().min(1).optional(), - priority: z.number().int(), + priority: z.int(), enabled: z.boolean().optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); registry.registerPath({ diff --git a/server/routers/role/addRoleAction.ts b/server/routers/role/addRoleAction.ts index 62ab87b5..74540b78 100644 --- a/server/routers/role/addRoleAction.ts +++ b/server/routers/role/addRoleAction.ts @@ -9,17 +9,13 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addRoleActionParamSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const addRoleActionParamSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const addRoleActionSchema = z - .object({ +const addRoleActionSchema = z.strictObject({ actionId: z.string() - }) - .strict(); + }); export async function addRoleAction( req: Request, diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index d268eed4..d33c733d 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -9,17 +9,13 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addRoleSiteParamsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const addRoleSiteParamsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const addRoleSiteSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const addRoleSiteSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function addRoleSite( req: Request, diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index f66c95e2..26573c6c 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -11,18 +11,14 @@ import { ActionsEnum } from "@server/auth/actions"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const createRoleParamsSchema = z - .object({ +const createRoleParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const createRoleSchema = z - .object({ +const createRoleSchema = z.strictObject({ name: z.string().min(1).max(255), description: z.string().optional() - }) - .strict(); + }); export const defaultRoleAllowedActions: ActionsEnum[] = [ ActionsEnum.getOrg, diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index 6806386e..e4d89b2f 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -10,17 +10,13 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteRoleSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const deleteRoleSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const deelteRoleBodySchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const deelteRoleBodySchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts index 66dbb68f..afd6e83a 100644 --- a/server/routers/role/getRole.ts +++ b/server/routers/role/getRole.ts @@ -10,11 +10,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getRoleSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const getRoleSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); registry.registerPath({ method: "get", diff --git a/server/routers/role/listRoleActions.ts b/server/routers/role/listRoleActions.ts index cdf1391b..8392c296 100644 --- a/server/routers/role/listRoleActions.ts +++ b/server/routers/role/listRoleActions.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleActionsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const listRoleActionsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function listRoleActions( req: Request, diff --git a/server/routers/role/listRoleResources.ts b/server/routers/role/listRoleResources.ts index ba254f1d..57a84c5c 100644 --- a/server/routers/role/listRoleResources.ts +++ b/server/routers/role/listRoleResources.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleResourcesSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const listRoleResourcesSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function listRoleResources( req: Request, diff --git a/server/routers/role/listRoleSites.ts b/server/routers/role/listRoleSites.ts index 72f49e3a..f35e367c 100644 --- a/server/routers/role/listRoleSites.ts +++ b/server/routers/role/listRoleSites.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listRoleSitesSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const listRoleSitesSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function listRoleSites( req: Request, diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index 56ae8a3a..14a5c2d1 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -11,11 +11,9 @@ import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; -const listRolesParamsSchema = z - .object({ +const listRolesParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listRolesSchema = z.object({ limit: z @@ -23,13 +21,13 @@ const listRolesSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); async function queryRoles(orgId: string, limit: number, offset: number) { diff --git a/server/routers/role/removeRoleAction.ts b/server/routers/role/removeRoleAction.ts index e643ae04..25fbaa29 100644 --- a/server/routers/role/removeRoleAction.ts +++ b/server/routers/role/removeRoleAction.ts @@ -9,17 +9,13 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleActionParamsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const removeRoleActionParamsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const removeRoleActionSchema = z - .object({ +const removeRoleActionSchema = z.strictObject({ actionId: z.string() - }) - .strict(); + }); export async function removeRoleAction( req: Request, diff --git a/server/routers/role/removeRoleResource.ts b/server/routers/role/removeRoleResource.ts index 4068b0bd..d2c7cae9 100644 --- a/server/routers/role/removeRoleResource.ts +++ b/server/routers/role/removeRoleResource.ts @@ -9,20 +9,16 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleResourceParamsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const removeRoleResourceParamsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const removeRoleResourceSchema = z - .object({ +const removeRoleResourceSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export async function removeRoleResource( req: Request, diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index 2670272d..8092eed1 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -9,17 +9,13 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeRoleSiteParamsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const removeRoleSiteParamsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const removeRoleSiteSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const removeRoleSiteSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function removeRoleSite( req: Request, diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 793be6eb..136ca389 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -9,20 +9,16 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const updateRoleParamsSchema = z - .object({ - roleId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const updateRoleParamsSchema = z.strictObject({ + roleId: z.string().transform(Number).pipe(z.int().positive()) + }); -const updateRoleBodySchema = z - .object({ +const updateRoleBodySchema = z.strictObject({ name: z.string().min(1).max(255).optional(), description: z.string().optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); export async function updateRole( diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index f98a01dc..81a35451 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -19,16 +19,13 @@ import { isIpInCidr } from "@server/lib/ip"; import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; -const createSiteParamsSchema = z - .object({ +const createSiteParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const createSiteSchema = z - .object({ +const createSiteSchema = z.strictObject({ name: z.string().min(1).max(255), - exitNodeId: z.number().int().positive().optional(), + exitNodeId: z.int().positive().optional(), // subdomain: z // .string() // .min(1) @@ -41,8 +38,7 @@ const createSiteSchema = z secret: z.string().optional(), address: z.string().optional(), type: z.enum(["newt", "wireguard", "local"]) - }) - .strict(); + }); // .refine((data) => { // if (data.type === "local") { // return !config.getRawConfig().flags?.disable_local_sites; diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 7a12e24a..a086e143 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -12,11 +12,9 @@ import { fromError } from "zod-validation-error"; import { sendToClient } from "#dynamic/routers/ws"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteSiteSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const deleteSiteSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index a9785fa4..b6ce346a 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -11,18 +11,16 @@ import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getSiteSchema = z - .object({ +const getSiteSchema = z.strictObject({ siteId: z .string() .optional() .transform(stoi) - .pipe(z.number().int().positive().optional()) + .pipe(z.int().positive().optional()) .optional(), niceId: z.string().optional(), orgId: z.string().optional() - }) - .strict(); + }); async function query(siteId?: number, niceId?: string, orgId?: string) { if (siteId) { diff --git a/server/routers/site/index.ts b/server/routers/site/index.ts index 3edf67c1..b97557a8 100644 --- a/server/routers/site/index.ts +++ b/server/routers/site/index.ts @@ -5,4 +5,4 @@ export * from "./updateSite"; export * from "./listSites"; export * from "./listSiteRoles"; export * from "./pickSiteDefaults"; -export * from "./socketIntegration"; +export * from "./socketIntegration"; \ No newline at end of file diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts index 009e0907..ec66d3c5 100644 --- a/server/routers/site/listSiteRoles.ts +++ b/server/routers/site/listSiteRoles.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const listSiteRolesSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const listSiteRolesSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function listSiteRoles( req: Request, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index cddf8c4b..f0854764 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -68,11 +68,9 @@ async function getLatestNewtVersion(): Promise { } } -const listSitesParamsSchema = z - .object({ +const listSitesParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listSitesSchema = z.object({ limit: z @@ -80,13 +78,13 @@ const listSitesSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function querySites(orgId: string, accessibleSiteIds: number[]) { diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index c4b3a087..029ae322 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -44,11 +44,9 @@ registry.registerPath({ responses: {} }); -const pickSiteDefaultsSchema = z - .object({ +const pickSiteDefaultsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); export async function pickSiteDefaults( req: Request, diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts index 3a52dcd2..33893000 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -46,18 +46,14 @@ export interface Container { networks: Record; } -const siteIdParamsSchema = z - .object({ - siteId: z.string().transform(stoi).pipe(z.number().int().positive()) - }) - .strict(); +const siteIdParamsSchema = z.strictObject({ + siteId: z.string().transform(stoi).pipe(z.int().positive()) + }); -const DockerStatusSchema = z - .object({ +const DockerStatusSchema = z.strictObject({ isAvailable: z.boolean(), socketPath: z.string().optional() - }) - .strict(); + }); function validateSiteIdParams(params: any) { const parsedParams = siteIdParamsSchema.safeParse(params); diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index e3724f36..4c25d4c5 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { sites } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -11,15 +11,13 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { isValidCIDR } from "@server/lib/validators"; -const updateSiteParamsSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const updateSiteParamsSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); -const updateSiteBodySchema = z - .object({ +const updateSiteBodySchema = z.strictObject({ name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional(), dockerSocketEnabled: z.boolean().optional(), remoteSubnets: z .string() @@ -36,9 +34,8 @@ const updateSiteBodySchema = z // megabytesIn: z.number().int().nonnegative().optional(), // megabytesOut: z.number().int().nonnegative().optional(), }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); registry.registerPath({ @@ -89,6 +86,29 @@ export async function updateSite( const { siteId } = parsedParams.data; const updateData = parsedBody.data; + // if niceId is provided, check if it's already in use by another site + if (updateData.niceId) { + const existingSite = await db + .select() + .from(sites) + .where( + and( + eq(sites.niceId, updateData.niceId), + eq(sites.orgId, sites.orgId) + ) + ) + .limit(1); + + if (existingSite.length > 0 && existingSite[0].siteId !== siteId) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A site with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs if (updateData.remoteSubnets) { const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim()); diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index f04d14e4..0187a62c 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -13,20 +13,17 @@ import { addTargets } from "../client/targets"; import { getUniqueSiteResourceName } from "@server/db/names"; import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; -const createSiteResourceParamsSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), +const createSiteResourceParamsSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); -const createSiteResourceSchema = z - .object({ +const createSiteResourceSchema = z.strictObject({ name: z.string().min(1).max(255), mode: z.enum(["host", "cidr", "port"]), protocol: z.enum(["tcp", "udp"]).optional(), - proxyPort: z.number().int().positive().optional(), - destinationPort: z.number().int().positive().optional(), + proxyPort: z.int().positive().optional(), + destinationPort: z.int().positive().optional(), destination: z.string().min(1), enabled: z.boolean().default(true), alias: z.string().optional() @@ -51,8 +48,8 @@ const createSiteResourceSchema = z .refine( (data) => { if (data.mode === "host") { - // Check if it's a valid IP address using zod - const isValidIP = z.string().ip().safeParse(data.destination).success; + // Check if it's a valid IP address using zod (v4 or v6) + const isValidIP = z.union([z.ipv4(), z.ipv6()]).safeParse(data.destination).success; // Check if it's a valid domain (hostname pattern, TLD not required) const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; @@ -70,8 +67,8 @@ const createSiteResourceSchema = z .refine( (data) => { if (data.mode === "cidr") { - // Check if it's a valid CIDR - const isValidCIDR = z.string().cidr().safeParse(data.destination).success; + // Check if it's a valid CIDR (v4 or v6) + const isValidCIDR = z.union([z.cidrv4(), z.cidrv6()]).safeParse(data.destination).success; return isValidCIDR; } return true; diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index c5dc7c18..4ee69bb5 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -12,16 +12,11 @@ import { OpenAPITags, registry } from "@server/openApi"; import { removeTargets } from "../client/targets"; import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; -const deleteSiteResourceParamsSchema = z - .object({ - siteResourceId: z - .string() - .transform(Number) - .pipe(z.number().int().positive()), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), +const deleteSiteResourceParamsSchema = z.strictObject({ + siteResourceId: z.string().transform(Number).pipe(z.int().positive()), + siteId: z.string().transform(Number).pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); export type DeleteSiteResourceResponse = { message: string; diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index 09c01eb0..48f10b8b 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -10,19 +10,17 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const getSiteResourceParamsSchema = z - .object({ +const getSiteResourceParamsSchema = z.strictObject({ siteResourceId: z .string() .optional() .transform((val) => val ? Number(val) : undefined) - .pipe(z.number().int().positive().optional()) + .pipe(z.int().positive().optional()) .optional(), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.int().positive()), niceId: z.string().optional(), orgId: z.string() - }) - .strict(); + }); async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) { if (siteResourceId && siteId && orgId) { diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index a17d3b0a..5de66505 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -10,11 +10,9 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const listAllSiteResourcesByOrgParamsSchema = z - .object({ +const listAllSiteResourcesByOrgParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); const listAllSiteResourcesByOrgQuerySchema = z.object({ limit: z @@ -22,13 +20,13 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export type ListAllSiteResourcesByOrgResponse = { diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index 7fdb7a85..e530952d 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -10,12 +10,10 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const listSiteResourcesParamsSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), +const listSiteResourcesParamsSchema = z.strictObject({ + siteId: z.string().transform(Number).pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); const listSiteResourcesQuerySchema = z.object({ limit: z @@ -23,13 +21,13 @@ const listSiteResourcesQuerySchema = z.object({ .optional() .default("100") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); export type ListSiteResourcesResponse = { diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 11ed35c7..7a8469ba 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -11,24 +11,21 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import { addTargets } from "../client/targets"; -const updateSiteResourceParamsSchema = z - .object({ +const updateSiteResourceParamsSchema = z.strictObject({ siteResourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), + .pipe(z.int().positive()), + siteId: z.string().transform(Number).pipe(z.int().positive()), orgId: z.string() - }) - .strict(); + }); -const updateSiteResourceSchema = z - .object({ +const updateSiteResourceSchema = z.strictObject({ name: z.string().min(1).max(255).optional(), mode: z.enum(["host", "cidr", "port"]).optional(), protocol: z.enum(["tcp", "udp"]).nullish(), - proxyPort: z.number().int().positive().nullish(), - destinationPort: z.number().int().positive().nullish(), + proxyPort: z.int().positive().nullish(), + destinationPort: z.int().positive().nullish(), destination: z.string().min(1).optional(), enabled: z.boolean().optional(), alias: z.string().nullish() diff --git a/server/routers/supporterKey/validateSupporterKey.ts b/server/routers/supporterKey/validateSupporterKey.ts index 9d949fb5..d8b16421 100644 --- a/server/routers/supporterKey/validateSupporterKey.ts +++ b/server/routers/supporterKey/validateSupporterKey.ts @@ -5,18 +5,14 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { response as sendResponse } from "@server/lib/response"; -import { suppressDeprecationWarnings } from "moment"; import { supporterKey } from "@server/db"; import { db } from "@server/db"; -import { eq } from "drizzle-orm"; import config from "@server/lib/config"; -const validateSupporterKeySchema = z - .object({ +const validateSupporterKeySchema = z.strictObject({ githubUsername: z.string().nonempty(), key: z.string().nonempty() - }) - .strict(); + }); export type ValidateSupporterKeyResponse = { valid: boolean; @@ -44,7 +40,7 @@ export async function validateSupporterKey( const { githubUsername, key } = parsedBody.data; const response = await fetch( - "https://api.fossorial.io/api/v1/license/validate", + `https://api.fossorial.io/api/v1/license/validate`, { method: "POST", headers: { diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index b35d8d2a..6cf29da3 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -15,44 +15,39 @@ import { pickPort } from "./helpers"; import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; -const createTargetParamsSchema = z - .object({ +const createTargetParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); -const createTargetSchema = z - .object({ - siteId: z.number().int().positive(), +const createTargetSchema = z.strictObject({ + siteId: z.int().positive(), ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), - port: z.number().int().min(1).max(65535), + port: z.int().min(1).max(65535), enabled: z.boolean().default(true), hcEnabled: z.boolean().optional(), hcPath: z.string().min(1).optional().nullable(), hcScheme: z.string().optional().nullable(), hcMode: z.string().optional().nullable(), hcHostname: z.string().optional().nullable(), - hcPort: z.number().int().positive().optional().nullable(), - hcInterval: z.number().int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z - .number() - .int() + hcPort: z.int().positive().optional().nullable(), + hcInterval: z.int().positive().min(5).optional().nullable(), + hcUnhealthyInterval: z.int() .positive() .min(5) .optional() .nullable(), - hcTimeout: z.number().int().positive().min(1).optional().nullable(), + hcTimeout: z.int().positive().min(1).optional().nullable(), hcHeaders: z - .array(z.object({ name: z.string(), value: z.string() })) + .array(z.strictObject({ name: z.string(), value: z.string() })) .nullable() .optional(), hcFollowRedirects: z.boolean().optional().nullable(), hcMethod: z.string().min(1).optional().nullable(), - hcStatus: z.number().int().optional().nullable(), + hcStatus: z.int().optional().nullable(), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) @@ -63,9 +58,8 @@ const createTargetSchema = z .enum(["exact", "prefix", "regex", "stripPrefix"]) .optional() .nullable(), - priority: z.number().int().min(1).max(1000).optional().nullable() - }) - .strict(); + priority: z.int().min(1).max(1000).optional().nullable() + }); export type CreateTargetResponse = Target & TargetHealthCheck; diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 596691e4..a70b2a1e 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -13,11 +13,9 @@ import { removeTargets } from "../newt/targets"; import { getAllowedIps } from "./helpers"; import { OpenAPITags, registry } from "@server/openApi"; -const deleteTargetSchema = z - .object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const deleteTargetSchema = z.strictObject({ + targetId: z.string().transform(Number).pipe(z.int().positive()) + }); registry.registerPath({ method: "delete", diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 864c02eb..7fe2e062 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -10,11 +10,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const getTargetSchema = z - .object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const getTargetSchema = z.strictObject({ + targetId: z.string().transform(Number).pipe(z.int().positive()) + }); type GetTargetResponse = Target & Omit & { hcHeaders: { name: string; value: string; }[] | null; diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 04966f6e..e97d577d 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -10,14 +10,12 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const listTargetsParamsSchema = z - .object({ +const listTargetsParamsSchema = z.strictObject({ resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); const listTargetsSchema = z.object({ limit: z @@ -25,13 +23,13 @@ const listTargetsSchema = z.object({ .optional() .default("1000") .transform(Number) - .pipe(z.number().int().positive()), + .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) + .pipe(z.int().nonnegative()) }); function queryTargets(resourceId: number) { diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 6e9a8fc9..1889154c 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -15,47 +15,41 @@ import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; import { vs } from "@react-email/components"; -const updateTargetParamsSchema = z - .object({ - targetId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); +const updateTargetParamsSchema = z.strictObject({ + targetId: z.string().transform(Number).pipe(z.int().positive()) + }); -const updateTargetBodySchema = z - .object({ - siteId: z.number().int().positive(), +const updateTargetBodySchema = z.strictObject({ + siteId: z.int().positive(), ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), - port: z.number().int().min(1).max(65535).optional(), + port: z.int().min(1).max(65535).optional(), enabled: z.boolean().optional(), hcEnabled: z.boolean().optional().nullable(), hcPath: z.string().min(1).optional().nullable(), hcScheme: z.string().optional().nullable(), hcMode: z.string().optional().nullable(), hcHostname: z.string().optional().nullable(), - hcPort: z.number().int().positive().optional().nullable(), - hcInterval: z.number().int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z - .number() - .int() + hcPort: z.int().positive().optional().nullable(), + hcInterval: z.int().positive().min(5).optional().nullable(), + hcUnhealthyInterval: z.int() .positive() .min(5) .optional() .nullable(), - hcTimeout: z.number().int().positive().min(1).optional().nullable(), - hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), + hcTimeout: z.int().positive().min(1).optional().nullable(), + hcHeaders: z.array(z.strictObject({ name: z.string(), value: z.string() })).nullable().optional(), hcFollowRedirects: z.boolean().optional().nullable(), hcMethod: z.string().min(1).optional().nullable(), - hcStatus: z.number().int().optional().nullable(), + hcStatus: z.int().optional().nullable(), path: z.string().optional().nullable(), pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), rewritePath: z.string().optional().nullable(), rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), - priority: z.number().int().min(1).max(1000).optional(), + priority: z.int().min(1).max(1000).optional(), }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); registry.registerPath({ diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 7e64770f..3e94d96c 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -14,12 +14,10 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; -const acceptInviteBodySchema = z - .object({ +const acceptInviteBodySchema = z.strictObject({ token: z.string(), inviteId: z.string() - }) - .strict(); + }); export type AcceptInviteResponse = { accepted: boolean; diff --git a/server/routers/user/addUserAction.ts b/server/routers/user/addUserAction.ts index 074ebe9b..f75d5005 100644 --- a/server/routers/user/addUserAction.ts +++ b/server/routers/user/addUserAction.ts @@ -9,13 +9,11 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addUserActionSchema = z - .object({ +const addUserActionSchema = z.strictObject({ userId: z.string(), actionId: z.string(), orgId: z.string() - }) - .strict(); + }); export async function addUserAction( req: Request, diff --git a/server/routers/user/addUserRole.ts b/server/routers/user/addUserRole.ts index 27f5e612..915ea64a 100644 --- a/server/routers/user/addUserRole.ts +++ b/server/routers/user/addUserRole.ts @@ -11,12 +11,10 @@ import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; -const addUserRoleParamsSchema = z - .object({ +const addUserRoleParamsSchema = z.strictObject({ userId: z.string(), roleId: z.string().transform(stoi).pipe(z.number()) - }) - .strict(); + }); export type AddUserRoleResponse = z.infer; diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index f094e20e..38ef264c 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -9,12 +9,10 @@ import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; -const addUserSiteSchema = z - .object({ +const addUserSiteSchema = z.strictObject({ userId: z.string(), - siteId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); + siteId: z.string().transform(Number).pipe(z.int().positive()) + }); export async function addUserSite( req: Request, diff --git a/server/routers/user/adminGetUser.ts b/server/routers/user/adminGetUser.ts index 0a961bec..bda14476 100644 --- a/server/routers/user/adminGetUser.ts +++ b/server/routers/user/adminGetUser.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -const adminGetUserSchema = z - .object({ +const adminGetUserSchema = z.strictObject({ userId: z.string().min(1) - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index 308b9def..a3ad9cdd 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -9,22 +9,20 @@ import logger from "@server/logger"; import { idp, users } from "@server/db"; import { fromZodError } from "zod-validation-error"; -const listUsersSchema = z - .object({ +const listUsersSchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function queryUsers(limit: number, offset: number) { return await db diff --git a/server/routers/user/adminRemoveUser.ts b/server/routers/user/adminRemoveUser.ts index 14916ab9..02ad56d6 100644 --- a/server/routers/user/adminRemoveUser.ts +++ b/server/routers/user/adminRemoveUser.ts @@ -9,11 +9,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserSchema = z - .object({ +const removeUserSchema = z.strictObject({ userId: z.string() - }) - .strict(); + }); export async function adminRemoveUser( req: Request, diff --git a/server/routers/user/adminUpdateUser2FA.ts b/server/routers/user/adminUpdateUser2FA.ts index becd2091..4bb2486a 100644 --- a/server/routers/user/adminUpdateUser2FA.ts +++ b/server/routers/user/adminUpdateUser2FA.ts @@ -10,17 +10,13 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const updateUser2FAParamsSchema = z - .object({ +const updateUser2FAParamsSchema = z.strictObject({ userId: z.string() - }) - .strict(); + }); -const updateUser2FABodySchema = z - .object({ +const updateUser2FABodySchema = z.strictObject({ twoFactorSetupRequested: z.boolean() - }) - .strict(); + }); export type UpdateUser2FAResponse = { userId: string; diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index 1e88add5..99a2258c 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -17,21 +17,17 @@ import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ orgId: z.string().nonempty() - }) - .strict(); + }); -const bodySchema = z - .object({ - email: z - .string() +const bodySchema = z.strictObject({ + email: z.email() .toLowerCase() .optional() .refine((data) => { if (data) { - return z.string().email().safeParse(data).success; + return z.email().safeParse(data).success; } return true; }), @@ -40,8 +36,7 @@ const bodySchema = z type: z.enum(["internal", "oidc"]).optional(), idpId: z.number().optional(), roleId: z.number() - }) - .strict(); + }); export type CreateOrgUserResponse = {}; diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 02ffd92c..4e09afd6 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -46,12 +46,10 @@ export type GetOrgUserResponse = NonNullable< Awaited> >; -const getOrgUserParamsSchema = z - .object({ +const getOrgUserParamsSchema = z.strictObject({ userId: z.string(), orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "get", diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 1cae46c9..f43ebeb8 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -21,21 +21,17 @@ import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import cache from "@server/lib/cache"; -const inviteUserParamsSchema = z - .object({ +const inviteUserParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const inviteUserBodySchema = z - .object({ - email: z.string().toLowerCase().email(), +const inviteUserBodySchema = z.strictObject({ + email: z.email().toLowerCase(), roleId: z.number(), validHours: z.number().gt(0).lte(168), sendEmail: z.boolean().optional(), regenerate: z.boolean().optional() - }) - .strict(); + }); export type InviteUserBody = z.infer; diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index c91a136d..a61e2372 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -10,28 +10,24 @@ import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listInvitationsParamsSchema = z - .object({ +const listInvitationsParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const listInvitationsQuerySchema = z - .object({ +const listInvitationsQuerySchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function queryInvitations(orgId: string, limit: number, offset: number) { return await db diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index a35da862..aa70874e 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -11,28 +11,24 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { eq } from "drizzle-orm"; -const listUsersParamsSchema = z - .object({ +const listUsersParamsSchema = z.strictObject({ orgId: z.string() - }) - .strict(); + }); -const listUsersSchema = z - .object({ +const listUsersSchema = z.strictObject({ limit: z .string() .optional() .default("1000") .transform(Number) - .pipe(z.number().int().nonnegative()), + .pipe(z.int().nonnegative()), offset: z .string() .optional() .default("0") .transform(Number) - .pipe(z.number().int().nonnegative()) - }) - .strict(); + .pipe(z.int().nonnegative()) + }); async function queryUsers(orgId: string, limit: number, offset: number) { return await db diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index e3ee40d0..44ec8c23 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -9,12 +9,10 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeInvitationParamsSchema = z - .object({ +const removeInvitationParamsSchema = z.strictObject({ orgId: z.string(), inviteId: z.string() - }) - .strict(); + }); export async function removeInvitation( req: Request, diff --git a/server/routers/user/removeUserAction.ts b/server/routers/user/removeUserAction.ts index f0bd7d92..6e4c1a66 100644 --- a/server/routers/user/removeUserAction.ts +++ b/server/routers/user/removeUserAction.ts @@ -9,18 +9,14 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserActionParamsSchema = z - .object({ +const removeUserActionParamsSchema = z.strictObject({ userId: z.string() - }) - .strict(); + }); -const removeUserActionSchema = z - .object({ +const removeUserActionSchema = z.strictObject({ actionId: z.string(), orgId: z.string() - }) - .strict(); + }); export async function removeUserAction( req: Request, diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index 8bad16d9..cbbb4495 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -15,12 +15,10 @@ import { build } from "@server/build"; import { UserType } from "@server/types/UserTypes"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; -const removeUserSchema = z - .object({ +const removeUserSchema = z.strictObject({ userId: z.string(), orgId: z.string() - }) - .strict(); + }); registry.registerPath({ method: "delete", diff --git a/server/routers/user/removeUserResource.ts b/server/routers/user/removeUserResource.ts index 186e8032..14dbb540 100644 --- a/server/routers/user/removeUserResource.ts +++ b/server/routers/user/removeUserResource.ts @@ -9,15 +9,13 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserResourceSchema = z - .object({ +const removeUserResourceSchema = z.strictObject({ userId: z.string(), resourceId: z .string() .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); + .pipe(z.int().positive()) + }); export async function removeUserResource( req: Request, diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 7dbb4a15..6ed2288a 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -9,17 +9,13 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -const removeUserSiteParamsSchema = z - .object({ +const removeUserSiteParamsSchema = z.strictObject({ userId: z.string() - }) - .strict(); + }); -const removeUserSiteSchema = z - .object({ - siteId: z.number().int().positive() - }) - .strict(); +const removeUserSiteSchema = z.strictObject({ + siteId: z.int().positive() + }); export async function removeUserSite( req: Request, diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts index fb00b59f..e1000063 100644 --- a/server/routers/user/updateOrgUser.ts +++ b/server/routers/user/updateOrgUser.ts @@ -9,20 +9,16 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const paramsSchema = z - .object({ +const paramsSchema = z.strictObject({ userId: z.string(), orgId: z.string() - }) - .strict(); + }); -const bodySchema = z - .object({ +const bodySchema = z.strictObject({ autoProvisioned: z.boolean().optional() }) - .strict() .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" + error: "At least one field must be provided for update" }); registry.registerPath({ diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx index 1d0a682f..73c6a3cf 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx @@ -76,8 +76,8 @@ export default function GeneralPage() { .min(1, { message: t("idpClientSecretRequired") }), roleMapping: z.string().nullable().optional(), roleId: z.number().nullable().optional(), - authUrl: z.string().url({ message: t("idpErrorAuthUrlInvalid") }), - tokenUrl: z.string().url({ message: t("idpErrorTokenUrlInvalid") }), + authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }), + tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }), identifierPath: z.string().min(1, { message: t("idpPathRequired") }), emailPath: z.string().nullable().optional(), namePath: z.string().nullable().optional(), diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index ba580ca0..8667abda 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -64,13 +64,9 @@ export default function Page() { clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), - authUrl: z - .string() - .url({ message: t("idpErrorAuthUrlInvalid") }) + authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }) .optional(), - tokenUrl: z - .string() - .url({ message: t("idpErrorTokenUrlInvalid") }) + tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }) .optional(), identifierPath: z .string() diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx index f9e8f7f9..62cada8d 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx @@ -239,6 +239,7 @@ export default function ExitNodesTable({ header: () => ({t("actions")}), cell: ({ row }) => { const nodeRow = row.original; + const remoteExitNodeId = nodeRow.id; return (
@@ -249,6 +250,14 @@ export default function ExitNodesTable({ + + + {t("viewSettings")} + + { setSelectedNode(nodeRow); @@ -261,6 +270,14 @@ export default function ExitNodesTable({ + + +
); } diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx new file mode 100644 index 00000000..115b1bd3 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useState } from "react"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useParams, useRouter } from "next/navigation"; +import { AxiosResponse } from "axios"; +import { useTranslations } from "next-intl"; +import { + PickRemoteExitNodeDefaultsResponse, + QuickStartRemoteExitNodeResponse +} from "@server/routers/remoteExitNode/types"; +import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { build } from "@server/build"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; + +export default function CredentialsPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + const { remoteExitNode } = useRemoteExitNodeContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [credentials, setCredentials] = useState(null); + + const { licenseStatus, isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + const isSecurityFeatureDisabled = () => { + const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); + const isSaasNotSubscribed = + build === "saas" && !subscription?.isSubscribed(); + return isEnterpriseNotLicensed || isSaasNotSubscribed; + }; + + + const handleConfirmRegenerate = async () => { + + const response = await api.get>( + `/org/${orgId}/pick-remote-exit-node-defaults` + ); + + const data = response.data.data; + setCredentials(data); + + await api.put>( + `/re-key/${orgId}/reGenerate-remote-exit-node-secret`, + { + remoteExitNodeId: remoteExitNode.remoteExitNodeId, + secret: data.secret, + } + ); + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + router.refresh(); + }; + + const getCredentials = () => { + if (credentials) { + return { + Id: remoteExitNode.remoteExitNodeId, + Secret: credentials.secret + }; + } + return undefined; + }; + + return ( + + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + + + + +
+ +
+
+ + {isSecurityFeatureDisabled() && ( + + {t("featureDisabledTooltip")} + + )} +
+
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx deleted file mode 100644 index 191ce3f3..00000000 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function GeneralPage() { - return <>; -} diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx index 7a7b3611..19357a7f 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx @@ -6,6 +6,8 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider"; +import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import ExitNodeInfoCard from "@app/components/ExitNodeInfoCard"; interface SettingsLayoutProps { children: React.ReactNode; @@ -31,6 +33,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const t = await getTranslations(); + const navItems = [ + { + title: t('credentials'), + href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials" + } + ]; + return ( <> -
{children}
+
+ + {children} +
); diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx index 6b39c1de..5b9fd628 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx @@ -5,6 +5,6 @@ export default async function RemoteExitNodePage(props: { }) { const params = await props.params; redirect( - `/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/general` + `/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/credentials` ); } diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index d789b2e2..9417282d 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -91,7 +91,7 @@ export default function Page() { const [dataLoaded, setDataLoaded] = useState(false); const internalFormSchema = z.object({ - email: z.string().email({ message: t("emailInvalid") }), + email: z.email({ message: t("emailInvalid") }), validForHours: z .string() .min(1, { message: t("inviteValidityDuration") }), @@ -99,16 +99,14 @@ export default function Page() { }); const googleAzureFormSchema = z.object({ - email: z.string().email({ message: t("emailInvalid") }), + email: z.email({ message: t("emailInvalid") }), name: z.string().optional(), roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }) }); const genericOidcFormSchema = z.object({ username: z.string().min(1, { message: t("usernameRequired") }), - email: z - .string() - .email({ message: t("emailInvalid") }) + email: z.email({ message: t("emailInvalid") }) .optional() .or(z.literal("")), name: z.string().optional(), diff --git a/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx new file mode 100644 index 00000000..f14d49e4 --- /dev/null +++ b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useState } from "react"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useParams, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { PickClientDefaultsResponse } from "@server/routers/client"; +import { useClientContext } from "@app/hooks/useClientContext"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import { build } from "@server/build"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; + +export default function CredentialsPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + const { client } = useClientContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [clientDefaults, setClientDefaults] = useState(null); + + const { licenseStatus, isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + const isSecurityFeatureDisabled = () => { + const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); + const isSaasNotSubscribed = + build === "saas" && !subscription?.isSubscribed(); + return isEnterpriseNotLicensed || isSaasNotSubscribed; + }; + + + const handleConfirmRegenerate = async () => { + + const res = await api.get(`/org/${orgId}/pick-client-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setClientDefaults(data); + + await api.post(`/re-key/${client?.clientId}/regenerate-client-secret`, { + olmId: data.olmId, + secret: data.olmSecret, + }); + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + router.refresh(); + } + }; + + const getCredentials = () => { + if (clientDefaults) { + return { + Id: clientDefaults.olmId, + Secret: clientDefaults.olmSecret + }; + } + return undefined; + }; + + return ( + + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + + + + +
+ +
+
+ + {isSecurityFeatureDisabled() && ( + + {t("featureDisabledTooltip")} + + )} +
+
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx index c9c9fd14..257cb20f 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx @@ -7,6 +7,8 @@ import ClientInfoCard from "../../../../../components/ClientInfoCard"; import ClientProvider from "@app/providers/ClientProvider"; import { redirect } from "next/navigation"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { getTranslations } from "next-intl/server"; +import { build } from "@server/build"; type SettingsLayoutProps = { children: React.ReactNode; @@ -30,11 +32,20 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/clients`); } + const t = await getTranslations(); + const navItems = [ { - title: "General", + title: t('general'), href: `/{orgId}/settings/clients/{clientId}/general` - } + }, + ...(build === 'enterprise' + ? [{ + title: t('credentials'), + href: `/{orgId}/settings/clients/{clientId}/credentials` + }, + ] + : []), ]; return ( diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 47657063..dfe08031 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -99,8 +99,12 @@ export default function Page() { id: z.string(), text: z.string() }) - ), - subnet: z.string().ip().min(1, { + ) + .refine((val) => val.length > 0, { + message: t("siteRequired") + }), + subnet: z.union([z.ipv4(), z.ipv6()]) + .refine((val) => val.length > 0, { message: t("subnetRequired") }) }); diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index 56c989c3..fe5f0ca2 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -921,9 +921,7 @@ export default function ResourceAuthenticationPage() { validateTag={( tag ) => { - return z - .string() - .email() + return z.email() .or( z .string() diff --git a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx index 28f7754b..1e1ff56b 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx @@ -68,9 +68,9 @@ export default function GeneralForm() { const router = useRouter(); const t = useTranslations(); const [editDomainOpen, setEditDomainOpen] = useState(false); - const {licenseStatus } = useLicenseStatusContext(); + const { licenseStatus } = useLicenseStatusContext(); const subscriptionStatus = useSubscriptionStatusContext(); - const {user} = useUserContext(); + const { user } = useUserContext(); const { env } = useEnvContext(); @@ -102,8 +102,9 @@ export default function GeneralForm() { enabled: z.boolean(), subdomain: z.string().optional(), name: z.string().min(1).max(255), + niceId: z.string().min(1).max(255).optional(), domainId: z.string().optional(), - proxyPort: z.number().int().min(1).max(65535).optional(), + proxyPort: z.int().min(1).max(65535).optional(), // enableProxy: z.boolean().optional() }) .refine( @@ -130,6 +131,7 @@ export default function GeneralForm() { defaultValues: { enabled: resource.enabled, name: resource.name, + niceId: resource.niceId, subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, proxyPort: resource.proxyPort || undefined, @@ -192,6 +194,7 @@ export default function GeneralForm() { { enabled: data.enabled, name: data.name, + niceId: data.niceId, subdomain: data.subdomain ? toASCII(data.subdomain) : undefined, domainId: data.domainId, proxyPort: data.proxyPort, @@ -212,16 +215,12 @@ export default function GeneralForm() { }); if (res && res.status === 200) { - toast({ - title: t("resourceUpdated"), - description: t("resourceUpdatedDescription") - }); - - const resource = res.data.data; + const updated = res.data.data; updateResource({ enabled: data.enabled, name: data.name, + niceId: data.niceId, subdomain: data.subdomain, fullDomain: resource.fullDomain, proxyPort: data.proxyPort, @@ -230,8 +229,20 @@ export default function GeneralForm() { // }) }); - router.refresh(); + toast({ + title: t("resourceUpdated"), + description: t("resourceUpdatedDescription") + }); + + if (data.niceId && data.niceId !== resource?.niceId) { + router.replace(`/${updated.orgId}/settings/resources/${data.niceId}/general`); + } else { + router.refresh(); + } + + setSaveLoading(false); } + setSaveLoading(false); } @@ -304,6 +315,24 @@ export default function GeneralForm() { )} /> + ( + + {t("identifier")} + + + + + + )} + /> + {!resource.http && ( <> ().int().positive(), + siteId: z.int() + .positive({ + error: "You must select a site for a target." + }), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) @@ -137,7 +137,7 @@ const addTargetSchema = z .enum(["exact", "prefix", "regex", "stripPrefix"]) .optional() .nullable(), - priority: z.number().int().min(1).max(1000).optional() + priority: z.int().min(1).max(1000).optional() }) .refine( (data) => { @@ -169,7 +169,7 @@ const addTargetSchema = z return true; }, { - message: "Invalid path configuration" + error: "Invalid path configuration" } ) .refine( @@ -185,7 +185,7 @@ const addTargetSchema = z return true; }, { - message: "Invalid rewrite path configuration" + error: "Invalid rewrite path configuration" } ); @@ -292,7 +292,7 @@ export default function ReverseProxyTargets(props: { .array(z.object({ name: z.string(), value: z.string() })) .nullable(), proxyProtocol: z.boolean().optional(), - proxyProtocolVersion: z.number().int().min(1).max(2).optional() + proxyProtocolVersion: z.int().min(1).max(2).optional() }); const tlsSettingsSchema = z.object({ @@ -512,9 +512,18 @@ export default function ReverseProxyTargets(props: { port: target.port, enabled: target.enabled, hcEnabled: target.hcEnabled, - hcPath: target.hcPath, - hcInterval: target.hcInterval, - hcTimeout: target.hcTimeout + hcPath: target.hcPath || null, + hcScheme: target.hcScheme || null, + hcHostname: target.hcHostname || null, + hcPort: target.hcPort || null, + hcInterval: target.hcInterval || null, + hcTimeout: target.hcTimeout || null, + hcHeaders: target.hcHeaders || null, + hcFollowRedirects: target.hcFollowRedirects || null, + hcMethod: target.hcMethod || null, + hcStatus: target.hcStatus || null, + hcUnhealthyInterval: target.hcUnhealthyInterval || null, + hcMode: target.hcMode || null }; // Only include path-related fields for HTTP resources @@ -718,7 +727,9 @@ export default function ReverseProxyTargets(props: { hcHeaders: target.hcHeaders || null, hcFollowRedirects: target.hcFollowRedirects || null, hcMethod: target.hcMethod || null, - hcStatus: target.hcStatus || null + hcStatus: target.hcStatus || null, + hcUnhealthyInterval: target.hcUnhealthyInterval || null, + hcMode: target.hcMode || null }; // Only include path-related fields for HTTP resources @@ -1822,6 +1833,7 @@ export default function ReverseProxyTargets(props: { 30 }} onChanges={async (config) => { + console.log("here"); if (selectedTargetForHealthCheck) { console.log(config); updateTargetHealthCheck( diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 592424e0..a464439b 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -93,7 +93,7 @@ const addRuleSchema = z.object({ action: z.enum(["ACCEPT", "DROP", "PASS"]), match: z.string(), value: z.string(), - priority: z.coerce.number().int().optional() + priority: z.coerce.number().int().optional() }); type LocalRule = ArrayElement & { @@ -440,9 +440,7 @@ export default function ResourceRules(props: { type="number" onClick={(e) => e.currentTarget.focus()} onBlur={(e) => { - const parsed = z.coerce - .number() - .int() + const parsed = z.int() .optional() .safeParse(e.target.value); diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index f0b7b79f..75e037be 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -128,7 +128,7 @@ const httpResourceFormSchema = z.object({ const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.int().min(1).max(65535) // enableProxy: z.boolean().default(false) }); @@ -136,8 +136,8 @@ const addTargetSchema = z .object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), - port: z.coerce.number().int().positive(), - siteId: z.number().int().positive(), + port: z.coerce.number().int().positive(), + siteId: z.int().positive(), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) @@ -148,7 +148,7 @@ const addTargetSchema = z .enum(["exact", "prefix", "regex", "stripPrefix"]) .optional() .nullable(), - priority: z.number().int().min(1).max(1000).optional() + priority: z.int().min(1).max(1000).optional() }) .refine( (data) => { @@ -180,7 +180,7 @@ const addTargetSchema = z return true; }, { - message: "Invalid path configuration" + error: "Invalid path configuration" } ) .refine( @@ -196,7 +196,7 @@ const addTargetSchema = z return true; }, { - message: "Invalid rewrite path configuration" + error: "Invalid rewrite path configuration" } ); @@ -574,7 +574,9 @@ export default function Page() { hcPort: target.hcPort || null, hcFollowRedirects: target.hcFollowRedirects || null, - hcStatus: target.hcStatus || null + hcStatus: target.hcStatus || null, + hcUnhealthyInterval: target.hcUnhealthyInterval || null, + hcMode: target.hcMode || null }; // Only include path-related fields for HTTP resources diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx new file mode 100644 index 00000000..6dcee413 --- /dev/null +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState } from "react"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useParams, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { PickSiteDefaultsResponse } from "@server/routers/site"; +import { useSiteContext } from "@app/hooks/useSiteContext"; +import { generateKeypair } from "../wireguardConfig"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { build } from "@server/build"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; + +export default function CredentialsPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + const { site } = useSiteContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [siteDefaults, setSiteDefaults] = useState(null); + const [wgConfig, setWgConfig] = useState(""); + const [publicKey, setPublicKey] = useState(""); + + const { licenseStatus, isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + const isSecurityFeatureDisabled = () => { + const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); + const isSaasNotSubscribed = + build === "saas" && !subscription?.isSubscribed(); + return isEnterpriseNotLicensed || isSaasNotSubscribed; + }; + + + const hydrateWireGuardConfig = ( + privateKey: string, + publicKey: string, + subnet: string, + address: string, + endpoint: string, + listenPort: string + ) => { + const config = `[Interface] +Address = ${subnet} +ListenPort = 51820 +PrivateKey = ${privateKey} + +[Peer] +PublicKey = ${publicKey} +AllowedIPs = ${address.split("/")[0]}/32 +Endpoint = ${endpoint}:${listenPort} +PersistentKeepalive = 5`; + setWgConfig(config); + return config; + }; + + const handleConfirmRegenerate = async () => { + let generatedPublicKey = ""; + let generatedWgConfig = ""; + + if (site?.type === "wireguard") { + const generatedKeypair = generateKeypair(); + generatedPublicKey = generatedKeypair.publicKey; + setPublicKey(generatedPublicKey); + + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setSiteDefaults(data); + + // generate config with the fetched data + generatedWgConfig = hydrateWireGuardConfig( + generatedKeypair.privateKey, + data.publicKey, + data.subnet, + data.address, + data.endpoint, + data.listenPort + ); + } + + await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, { + type: "wireguard", + subnet: res.data.data.subnet, + exitNodeId: res.data.data.exitNodeId, + pubKey: generatedPublicKey + }); + } + + if (site?.type === "newt") { + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setSiteDefaults(data); + + await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, { + type: "newt", + newtId: data.newtId, + newtSecret: data.newtSecret + }); + } + } + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + router.refresh(); + }; + + const getCredentialType = () => { + if (site?.type === "wireguard") return "site-wireguard"; + if (site?.type === "newt") return "site-newt"; + return "site-newt"; + }; + + const getCredentials = () => { + if (site?.type === "wireguard" && wgConfig) { + return { wgConfig }; + } + if (site?.type === "newt" && siteDefaults) { + return { + Id: siteDefaults.newtId, + Secret: siteDefaults.newtSecret + }; + } + return undefined; + }; + + return ( + + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + + + + +
+ +
+
+ + {isSecurityFeatureDisabled() && ( + + {t("featureDisabledTooltip")} + + )} +
+
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 539941f9..f0df26c7 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -15,7 +15,7 @@ import { import { Input } from "@/components/ui/input"; import { useSiteContext } from "@app/hooks/useSiteContext"; import { useForm } from "react-hook-form"; -import { toast } from "@app/hooks/useToast"; +import { toast, useToast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { SettingsContainer, @@ -36,7 +36,8 @@ import Link from "next/link"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), - dockerSocketEnabled: z.boolean().optional() + niceId: z.string().min(1).max(255).optional(), + dockerSocketEnabled: z.boolean().optional(), }); type GeneralFormValues = z.infer; @@ -46,20 +47,19 @@ export default function GeneralPage() { const { env } = useEnvContext(); const api = createApiClient(useEnvContext()); - - const [loading, setLoading] = useState(false); - const [activeCidrTagIndex, setActiveCidrTagIndex] = useState( - null - ); - const router = useRouter(); const t = useTranslations(); + const { toast } = useToast(); + + const [loading, setLoading] = useState(false); + const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: site?.name, - dockerSocketEnabled: site?.dockerSocketEnabled ?? false + niceId: site?.niceId || "", + dockerSocketEnabled: site?.dockerSocketEnabled ?? false, }, mode: "onChange" }); @@ -67,31 +67,34 @@ export default function GeneralPage() { async function onSubmit(data: GeneralFormValues) { setLoading(true); - await api - .post(`/site/${site?.siteId}`, { + try { + await api.post(`/site/${site?.siteId}`, { name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("siteErrorUpdate"), - description: formatAxiosError( - e, - t("siteErrorUpdateDescription") - ) - }); + niceId: data.niceId, + dockerSocketEnabled: data.dockerSocketEnabled, }); - updateSite({ - name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled - }); + updateSite({ + name: data.name, + niceId: data.niceId, + dockerSocketEnabled: data.dockerSocketEnabled, + }); - toast({ - title: t("siteUpdated"), - description: t("siteUpdatedDescription") - }); + if (data.niceId && data.niceId !== site?.niceId) { + router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`); + } + + toast({ + title: t("siteUpdated"), + description: t("siteUpdatedDescription") + }); + } catch (e) { + toast({ + variant: "destructive", + title: t("siteErrorUpdate"), + description: formatAxiosError(e, t("siteErrorUpdateDescription")) + }); + } setLoading(false); @@ -132,6 +135,24 @@ export default function GeneralPage() { )} /> + ( + + {t("identifier")} + + + + + + )} + /> + {site && site.type === "newt" && ( diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 6274cda5..7eae6950 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -61,8 +61,8 @@ export default function GeneralPage() { name: z.string().min(2, { message: t('nameMin', {len: 2}) }), clientId: z.string().min(1, { message: t('idpClientIdRequired') }), clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), + authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }), + tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }), identifierPath: z .string() .min(1, { message: t('idpPathRequired') }), diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index cd3682de..73d605a1 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -52,8 +52,8 @@ export default function Page() { type: z.enum(["oidc"]), clientId: z.string().min(1, { message: t('idpClientIdRequired') }), clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), + authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }), + tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }), identifierPath: z .string() .min(1, { message: t('idpPathRequired') }), diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 14199493..986c52e4 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -47,7 +47,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; const requestSchema = z.object({ - email: z.string().email() + email: z.email() }); export type ResetPasswordFormProps = { @@ -88,7 +88,7 @@ export default function ResetPasswordForm({ const formSchema = z .object({ - email: z.string().email({ message: t('emailInvalid') }), + email: z.email({ message: t('emailInvalid') }), token: z.string().min(8, { message: t('tokenInvalid') }), password: passwordSchema, confirmPassword: passwordSchema diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cc586dfb..c8907a49 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -20,6 +20,7 @@ import { Toaster } from "@app/components/ui/toaster"; import { build } from "@server/build"; import { TopLoader } from "@app/components/Toploader"; import Script from "next/script"; +import { ReactQueryProvider } from "@app/components/react-query-provider"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -94,38 +95,40 @@ export default async function RootLayout({ strategy="afterInteractive" /> )} - - - - - - + + + + + - {/* Main content */} -
-
- + + {/* Main content */} +
+
+ + + {children} + - {children} - - +
-
- - - - - - - + + + + + + + + ); diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx index 39857214..f8d96158 100644 --- a/src/components/ClientInfoCard.tsx +++ b/src/components/ClientInfoCard.tsx @@ -1,7 +1,6 @@ "use client"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { useClientContext } from "@app/hooks/useClientContext"; import { InfoSection, diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index fc25f187..9fc35291 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -86,20 +86,15 @@ export default function CreateInternalResourceDialog({ .string() .min(1, t("createInternalResourceDialogNameRequired")) .max(255, t("createInternalResourceDialogNameMaxLength")), - siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), mode: z.enum(["host", "cidr", "port"]), - protocol: z.enum(["tcp", "udp"]).nullish(), - proxyPort: z - .number() - .int() + destination: z.string().min(1), + siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z.int() .positive() .min(1, t("createInternalResourceDialogProxyPortMin")) - .max(65535, t("createInternalResourceDialogProxyPortMax")) - .nullish(), - destination: z.string().min(1), - destinationPort: z - .number() - .int() + .max(65535, t("createInternalResourceDialogProxyPortMax")), + destinationPort: z.int() .positive() .min(1, t("createInternalResourceDialogDestinationPortMin")) .max(65535, t("createInternalResourceDialogDestinationPortMax")) diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 51cc52ab..3cc203f3 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -108,7 +108,7 @@ export default function CreateShareLinkForm({ resourceName: z.string(), resourceUrl: z.string(), timeUnit: z.string(), - timeValue: z.coerce.number().int().positive().min(1), + timeValue: z.coerce.number().int().positive().min(1), title: z.string().optional() }); diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx index 83325cc0..bc764eb9 100644 --- a/src/components/DNSRecordTable.tsx +++ b/src/components/DNSRecordTable.tsx @@ -6,6 +6,7 @@ import { useTranslations } from "next-intl"; import { Badge } from "@app/components/ui/badge"; import { DNSRecordsDataTable } from "./DNSRecordsDataTable"; import CopyToClipboard from "@app/components/CopyToClipboard"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type DNSRecordRow = { id: string; @@ -25,6 +26,30 @@ export default function DNSRecordsTable({ type }: Props) { const t = useTranslations(); + const env = useEnvContext(); + + const statusColumn: ColumnDef = { + accessorKey: "verified", + header: ({ column }) => { + return
{t("status")}
; + }, + cell: ({ row }) => { + const verified = row.original.verified; + return verified ? ( + type === "wildcard" ? ( + + {t("manual", { fallback: "Manual" })} + + ) : ( + {t("verified")} + ) + ) : ( + + {t("pending", { fallback: "Pending" })} + + ); + } + }; const columns: ExtendedColumnDef[] = [ { @@ -86,29 +111,7 @@ export default function DNSRecordsTable({ ); } }, - { - accessorKey: "verified", - friendlyName: t("status"), - header: ({ column }) => { - return
{t("status")}
; - }, - cell: ({ row }) => { - const verified = row.original.verified; - return verified ? ( - type === "wildcard" ? ( - - {t("manual", { fallback: "Manual" })} - - ) : ( - {t("verified")} - ) - ) : ( - - {t("pending", { fallback: "Pending" })} - - ); - } - } + ...(env.env.flags.usePangolinDns ? [statusColumn] : []) ]; return ( diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index b5de6cd3..e33998da 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -9,6 +9,7 @@ import { } from "@app/components/InfoSection"; import { useTranslations } from "next-intl"; import { Badge } from "./ui/badge"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type DomainInfoCardProps = { failed: boolean; @@ -22,6 +23,7 @@ export default function DomainInfoCard({ type }: DomainInfoCardProps) { const t = useTranslations(); + const env = useEnvContext(); const getTypeDisplay = (type: string) => { switch (type) { @@ -46,32 +48,34 @@ export default function DomainInfoCard({ {getTypeDisplay(type ? type : "")} - - {t("status")} - - {failed ? ( - - {t("failed", { fallback: "Failed" })} - - ) : verified ? ( - type === "wildcard" ? ( - - {t("manual", { - fallback: "Manual" - })} + {env.env.flags.usePangolinDns && ( + + {t("status")} + + {failed ? ( + + {t("failed", { fallback: "Failed" })} + ) : verified ? ( + type === "wildcard" ? ( + + {t("manual", { + fallback: "Manual" + })} + + ) : ( + + {t("verified")} + + ) ) : ( - - {t("verified")} + + {t("pending", { fallback: "Pending" })} - ) - ) : ( - - {t("pending", { fallback: "Pending" })} - - )} - - + )} + + + )} diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index 3f89190b..b76095f4 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -56,7 +56,8 @@ export default function DomainsTable({ domains, orgId }: Props) { const [restartingDomains, setRestartingDomains] = useState>( new Set() ); - const api = createApiClient(useEnvContext()); + const env = useEnvContext(); + const api = createApiClient(env); const router = useRouter(); const t = useTranslations(); const { toast } = useToast(); @@ -135,6 +136,41 @@ export default function DomainsTable({ domains, orgId }: Props) { } }; + const statusColumn: ColumnDef = { + accessorKey: "verified", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const { verified, failed, type } = row.original; + if (verified) { + return type == "wildcard" ? ( + {t("manual")} + ) : ( + {t("verified")} + ); + } else if (failed) { + return ( + + {t("failed", { fallback: "Failed" })} + + ); + } else { + return {t("pending")}; + } + } + }; + const columns: ExtendedColumnDef[] = [ { accessorKey: "baseDomain", @@ -177,41 +213,7 @@ export default function DomainsTable({ domains, orgId }: Props) { ); } }, - { - accessorKey: "verified", - friendlyName: t("status"), - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const { verified, failed, type } = row.original; - if (verified) { - return type == "wildcard" ? ( - {t("manual")} - ) : ( - {t("verified")} - ); - } else if (failed) { - return ( - - {t("failed", { fallback: "Failed" })} - - ); - } else { - return {t("pending")}; - } - } - }, + ...(env.env.flags.usePangolinDns ? [statusColumn] : []), { id: "actions", enableHiding: false, diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 679cbb9e..1508cc12 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -84,9 +84,9 @@ export default function EditInternalResourceDialog({ name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), mode: z.enum(["host", "cidr", "port"]), protocol: z.enum(["tcp", "udp"]).nullish(), - proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), + proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), destination: z.string().min(1), - destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), + destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), roles: z.array( z.object({ diff --git a/src/components/ExitNodeInfoCard.tsx b/src/components/ExitNodeInfoCard.tsx new file mode 100644 index 00000000..49ae1b61 --- /dev/null +++ b/src/components/ExitNodeInfoCard.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; +import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext"; + +type ExitNodeInfoCardProps = {}; + +export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) { + const { remoteExitNode, updateRemoteExitNode } = useRemoteExitNodeContext(); + const t = useTranslations(); + + return ( + + + + <> + + {t("status")} + + {remoteExitNode.online ? ( +
+
+ {t("online")} +
+ ) : ( +
+
+ {t("offline")} +
+ )} +
+
+ + + {t("address")} + + {remoteExitNode.address} + + +
+
+
+ ); +} diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx index 7dfd34ee..6a380082 100644 --- a/src/components/GenerateLicenseKeyForm.tsx +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -63,7 +63,7 @@ export default function GenerateLicenseKeyForm({ // Personal form schema const personalFormSchema = z.object({ - email: z.string().email(), + email: z.email(), firstName: z.string().min(1), lastName: z.string().min(1), primaryUse: z.string().min(1), @@ -75,14 +75,14 @@ export default function GenerateLicenseKeyForm({ // Business form schema const businessFormSchema = z.object({ - email: z.string().email(), + email: z.email(), firstName: z.string().min(1), lastName: z.string().min(1), jobTitle: z.string().min(1), primaryUse: z.string().min(1), industry: z.string().min(1), - prospectiveUsers: z.coerce.number().optional(), - prospectiveSites: z.coerce.number().optional(), + prospectiveUsers: z.coerce.number().optional(), + prospectiveSites: z.coerce.number().optional(), companyName: z.string().min(1), countryOfResidence: z.string().min(1), stateProvinceRegion: z.string().min(1), diff --git a/src/components/HealthCheckDialog.tsx b/src/components/HealthCheckDialog.tsx index 6fa36a5b..be5e5d45 100644 --- a/src/components/HealthCheckDialog.tsx +++ b/src/components/HealthCheckDialog.tsx @@ -80,24 +80,20 @@ export default function HealthCheckDialog({ hcMethod: z .string() .min(1, { message: t("healthCheckMethodRequired") }), - hcInterval: z - .number() - .int() + hcInterval: z.int() .positive() .min(5, { message: t("healthCheckIntervalMin") }), - hcTimeout: z - .number() - .int() + hcTimeout: z.int() .positive() .min(1, { message: t("healthCheckTimeoutMin") }), - hcStatus: z.number().int().positive().min(100).optional().nullable(), + hcStatus: z.int().positive().min(100).optional().nullable(), hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), hcScheme: z.string().optional(), hcHostname: z.string(), hcPort: z.number().positive().gt(0).lte(65535), hcFollowRedirects: z.boolean(), hcMode: z.string(), - hcUnhealthyInterval: z.number().int().positive().min(5) + hcUnhealthyInterval: z.int().positive().min(5) }); const form = useForm>({ diff --git a/src/components/IdpCreateWizard.tsx b/src/components/IdpCreateWizard.tsx index 937bd309..58093e6c 100644 --- a/src/components/IdpCreateWizard.tsx +++ b/src/components/IdpCreateWizard.tsx @@ -59,8 +59,8 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id type: z.enum(["oidc"]), clientId: z.string().min(1, { message: t('idpClientIdRequired') }), clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), + authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }), + tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }), identifierPath: z .string() .min(1, { message: t('idpPathRequired') }), diff --git a/src/components/InfoSection.tsx b/src/components/InfoSection.tsx index 41f5cd89..5959bfc3 100644 --- a/src/components/InfoSection.tsx +++ b/src/components/InfoSection.tsx @@ -50,5 +50,11 @@ export function InfoSectionContent({ children: React.ReactNode; className?: string; }) { - return
{children}
; + return ( +
+
+ {children} +
+
+ ); } diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index daaf6a9c..6951040e 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -32,6 +32,11 @@ import { import { build } from "@server/build"; import SidebarLicenseButton from "./SidebarLicenseButton"; import { SidebarSupportButton } from "./SidebarSupportButton"; +import dynamic from "next/dynamic"; + +const ProductUpdates = dynamic(() => import("./ProductUpdates"), { + ssr: false +}); interface LayoutSidebarProps { orgId?: string; @@ -101,7 +106,7 @@ export function LayoutSidebar({ @@ -133,7 +138,9 @@ export function LayoutSidebar({
-
+
+ + {build === "enterprise" && (
- +
)} {!isSidebarCollapsed && ( diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx new file mode 100644 index 00000000..ac2c22d8 --- /dev/null +++ b/src/components/ProductUpdates.tsx @@ -0,0 +1,379 @@ +"use client"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useLocalStorage } from "@app/hooks/useLocalStorage"; +import { cn } from "@app/lib/cn"; +import { + type LatestVersionResponse, + type ProductUpdate, + productUpdatesQueries +} from "@app/lib/queries"; +import { useQueries } from "@tanstack/react-query"; +import { + ArrowRight, + BellIcon, + ChevronRightIcon, + ExternalLinkIcon, + RocketIcon, + XIcon +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { Transition } from "@headlessui/react"; +import * as React from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { Button } from "./ui/button"; +import { Badge } from "./ui/badge"; +import { timeAgoFormatter } from "@app/lib/timeAgoFormatter"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "./ui/tooltip"; + +export default function ProductUpdates({ + isCollapsed +}: { + isCollapsed?: boolean; +}) { + const { env } = useEnvContext(); + + const data = useQueries({ + queries: [ + productUpdatesQueries.list(env.app.notifications.product_updates), + productUpdatesQueries.latestVersion( + env.app.notifications.new_releases + ) + ], + combine(result) { + if (result[0].isLoading || result[1].isLoading) return null; + return { + updates: result[0].data?.data ?? [], + latestVersion: result[1].data + }; + } + }); + const t = useTranslations(); + const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false); + + // we delay the small text animation so that the user can notice it + React.useEffect(() => { + const timeout = setTimeout(() => setShowMoreUpdatesText(true), 600); + return () => clearTimeout(timeout); + }, []); + + const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage< + string | null + >("product-updates:skip-version", null); + + const [productUpdatesRead, setProductUpdatesRead] = useLocalStorage< + number[] + >("product-updates:read", []); + + if (!data) return null; + + const showNewVersionPopup = Boolean( + data?.latestVersion?.data && + ignoredVersionUpdate !== + data.latestVersion.data?.pangolin.latestVersion && + env.app.version !== data.latestVersion.data?.pangolin.latestVersion + ); + + const filteredUpdates = data.updates.filter( + (update) => !productUpdatesRead.includes(update.id) + ); + + return ( +
+
+ + {filteredUpdates.length > 0 && ( + <> + + + {showNewVersionPopup + ? t("productUpdateMoreInfo", { + noOfUpdates: filteredUpdates.length + }) + : t("productUpdateInfo", { + noOfUpdates: filteredUpdates.length + })} + + + )} + + 0} + onDimissAll={() => + setProductUpdatesRead([ + ...productUpdatesRead, + ...filteredUpdates.map((update) => update.id) + ]) + } + onDimiss={(id) => + setProductUpdatesRead([...productUpdatesRead, id]) + } + /> +
+ + { + setIgnoredVersionUpdate( + data.latestVersion?.data?.pangolin.latestVersion ?? null + ); + }} + show={showNewVersionPopup} + /> +
+ ); +} + +type ProductUpdatesListPopupProps = { + updates: ProductUpdate[]; + show: boolean; + onDimiss: (id: number) => void; + onDimissAll: () => void; +}; + +function ProductUpdatesListPopup({ + updates, + show, + onDimiss, + onDimissAll +}: ProductUpdatesListPopupProps) { + const [showContent, setShowContent] = React.useState(false); + const [popoverOpen, setPopoverOpen] = React.useState(false); + const t = useTranslations(); + + // we need to delay the initial opening state to have an animation on `appear` + React.useEffect(() => { + if (show) { + requestAnimationFrame(() => setShowContent(true)); + } + }, [show]); + + React.useEffect(() => { + if (updates.length === 0) { + setShowContent(false); + setPopoverOpen(false); + } + }, [updates.length]); + + return ( + + + +
+
+ +
+
+
+

+ {t("productUpdateWhatsNew")} +

+
+ +
+
+ + {updates[0]?.contents} + +
+
+
+
+ +
+ + {t("productUpdateTitle")} + {updates.length > 0 && ( + {updates.length} + )} + + +
+
    + {updates.length === 0 && ( + + {t("productUpdateEmpty")} + + )} + {updates.map((update) => ( +
  1. +
    +

    + {update.title} + + {update.type} + +

    + + + + + + + {t("dismiss")} + + + +
    +
    + + {update.contents}{" "} + {update.link && ( + + Read more{" "} + + + )} + +
    + +
  2. + ))} +
+
+
+ ); +} + +type NewVersionAvailableProps = { + onDimiss: () => void; + show: boolean; + version: LatestVersionResponse | null | undefined; +}; + +function NewVersionAvailable({ + version, + show, + onDimiss +}: NewVersionAvailableProps) { + const t = useTranslations(); + const [open, setOpen] = React.useState(false); + + // we need to delay the initial opening state to have an animation on `appear` + React.useEffect(() => { + if (show) { + requestAnimationFrame(() => setOpen(true)); + } + }, [show]); + + return ( + +
+ {version && ( + <> +
+ +
+
+

+ {t("pangolinUpdateAvailable")} +

+ + {t("pangolinUpdateAvailableInfo", { + version: version.pangolin.latestVersion + })} + + + + {t("pangolinUpdateAvailableReleaseNotes")} + + + +
+ + + )} +
+
+ ); +} diff --git a/src/components/RegenerateCredentialsModal.tsx b/src/components/RegenerateCredentialsModal.tsx new file mode 100644 index 00000000..f485746b --- /dev/null +++ b/src/components/RegenerateCredentialsModal.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState } from "react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { Button } from "@app/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, AlertTriangle } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { QRCodeCanvas } from "qrcode.react"; + +type CredentialType = "site-wireguard" | "site-newt" | "client-olm" | "remote-exit-node"; + +interface RegenerateCredentialsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + type: CredentialType; + onConfirmRegenerate: () => Promise; + dashboardUrl: string; + credentials?: { + // For WireGuard sites + wgConfig?: string; + + Id?: string; + Secret?: string; + }; +} + +export default function RegenerateCredentialsModal({ + open, + onOpenChange, + type, + onConfirmRegenerate, + dashboardUrl, + credentials +}: RegenerateCredentialsModalProps) { + const t = useTranslations(); + const [stage, setStage] = useState<"confirm" | "show">("confirm"); + const [loading, setLoading] = useState(false); + + const handleConfirm = async () => { + try { + setLoading(true); + await onConfirmRegenerate(); + setStage("show"); + } catch (error) { + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setStage("confirm"); + onOpenChange(false); + }; + + const getTitle = () => { + if (stage === "confirm") { + return t("regeneratecredentials"); + } + switch (type) { + case "site-wireguard": + return t("WgConfiguration"); + case "site-newt": + return t("siteNewtCredentials"); + case "client-olm": + return t("clientOlmCredentials"); + case "remote-exit-node": + return t("remoteExitNodeCreate.generate.title"); + } + }; + + const getDescription = () => { + if (stage === "confirm") { + return t("regenerateCredentialsWarning"); + } + switch (type) { + case "site-wireguard": + return t("WgConfigurationDescription"); + case "site-newt": + return t("siteNewtCredentialsDescription"); + case "client-olm": + return t("clientOlmCredentialsDescription"); + case "remote-exit-node": + return t("remoteExitNodeCreate.generate.description"); + } + }; + + return ( + + + + {getTitle()} + {getDescription()} + + + + {stage === "confirm" ? ( + + + + {t("warning")} + + + {t("regenerateCredentialsConfirmation")} + + + ) : ( + <> + {credentials?.wgConfig && ( +
+
+ +
+
+ +
+
+
+ + + + + {t("copyandsavethesecredentials")} + + + {t("copyandsavethesecredentialsdescription")} + + +
+ )} + + {credentials?.Id && credentials.Secret && ( +
+ + + + {t("endpoint")} + + + + + + + + {t("Id")} + + + + + + + + {t("SecretKey")} + + + + + + + + + + {t("copyandsavethesecredentials")} + + + {t("copyandsavethesecredentialsdescription")} + + +
+ + )} + + )} +
+ + + {stage === "confirm" ? ( + <> + + + + + + ) : ( + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ResetPasswordForm.tsx b/src/components/ResetPasswordForm.tsx index e3e677b0..570a87ec 100644 --- a/src/components/ResetPasswordForm.tsx +++ b/src/components/ResetPasswordForm.tsx @@ -47,7 +47,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; const requestSchema = z.object({ - email: z.string().email() + email: z.email() }); export type ResetPasswordFormProps = { @@ -88,7 +88,7 @@ export default function ResetPasswordForm({ const formSchema = z .object({ - email: z.string().email({ message: t('emailInvalid') }), + email: z.email({ message: t('emailInvalid') }), token: z.string().min(8, { message: t('tokenInvalid') }), password: passwordSchema, confirmPassword: passwordSchema diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index 50bb9af2..0696c605 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -1,7 +1,7 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; +import { ShieldCheck, ShieldOff, Eye, EyeOff } from "lucide-react"; import { useResourceContext } from "@app/hooks/useResourceContext"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { @@ -17,21 +17,30 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; type ResourceInfoBoxType = {}; -export default function ResourceInfoBox({}: ResourceInfoBoxType) { - const { resource, authInfo } = useResourceContext(); +export default function ResourceInfoBox({ }: ResourceInfoBoxType) { + const { resource, authInfo, updateResource } = useResourceContext(); const { env } = useEnvContext(); const t = useTranslations(); const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`; + return ( {/* 4 cols because of the certs */} + + + {t("identifier")} + + + {resource.niceId} + + {resource.http ? ( <> @@ -40,17 +49,17 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { {authInfo.password || - authInfo.pincode || - authInfo.sso || - authInfo.whitelist || - authInfo.headerAuth ? ( -
- + authInfo.pincode || + authInfo.sso || + authInfo.whitelist || + authInfo.headerAuth ? ( +
+ {t("protected")}
) : (
- + {t("notProtected")}
)} @@ -91,9 +100,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { {t("protocol")} - - {resource.protocol.toUpperCase()} - + {resource.protocol.toUpperCase()} @@ -155,11 +162,17 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { {t("visibility")} - - {resource.enabled - ? t("enabled") - : t("disabled")} - + {resource.enabled ? ( +
+ + {t("enabled")} +
+ ) : ( +
+ + {t("disabled")} +
+ )}
diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index ef16f728..6d4c1e47 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -18,9 +18,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - DropdownMenuCheckboxItem, DropdownMenuLabel, - DropdownMenuSeparator + DropdownMenuSeparator, + DropdownMenuCheckboxItem } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { @@ -153,7 +153,7 @@ function StatusIcon({ case "offline": return ; case "unknown": - return ; + return ; default: return null; } @@ -491,8 +491,8 @@ export default function ResourcesTable({ return (
- - No targets + + {t("resourcesTableNoTargets")}
); @@ -511,14 +511,14 @@ export default function ResourcesTable({ @@ -572,8 +572,8 @@ export default function ResourcesTable({
{!target.enabled - ? "Disabled" - : "Not monitored"} + ? t("disabled") + : t("resourcesTableNotMonitored")} ))} @@ -606,6 +606,7 @@ export default function ResourcesTable({ { accessorKey: "nice", friendlyName: t("resource"), + enableHiding: true, header: ({ column }) => { return ( - ); - }, - cell: ({ row }) => { - return ( -
- {row.original.nice} -
- ); - } - }, { accessorKey: "mbIn", friendlyName: t("dataIn"), diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx index 5a1a10bc..baa3721c 100644 --- a/src/components/SupporterStatus.tsx +++ b/src/components/SupporterStatus.tsx @@ -74,8 +74,12 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus const formSchema = z.object({ githubUsername: z .string() - .nonempty({ message: "GitHub username is required" }), - key: z.string().nonempty({ message: "Supporter key is required" }) + .nonempty({ + error: "GitHub username is required" + }), + key: z.string().nonempty({ + error: "Supporter key is required" + }) }); const form = useForm({ diff --git a/src/components/VerifyEmailForm.tsx b/src/components/VerifyEmailForm.tsx index 48786bcd..14a362df 100644 --- a/src/components/VerifyEmailForm.tsx +++ b/src/components/VerifyEmailForm.tsx @@ -74,7 +74,7 @@ export default function VerifyEmailForm({ } const FormSchema = z.object({ - email: z.string().email({ message: t("emailInvalid") }), + email: z.email({ message: t("emailInvalid") }), pin: z.string().min(8, { message: t("verificationCodeLengthRequirements") }) diff --git a/src/components/react-query-provider.tsx b/src/components/react-query-provider.tsx new file mode 100644 index 00000000..0f65ba62 --- /dev/null +++ b/src/components/react-query-provider.tsx @@ -0,0 +1,29 @@ +"use client"; +import * as React from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { QueryClient } from "@tanstack/react-query"; + +export type ReactQueryProviderProps = { + children: React.ReactNode; +}; + +export function ReactQueryProvider({ children }: ReactQueryProviderProps) { + const [queryClient] = React.useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: 2, // retry twice by default + staleTime: 5 * 60 * 1_000 // 5 minutes + } + } + }) + ); + return ( + + {children} + + + ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 3bcf2bea..50ba04e0 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -10,7 +10,8 @@ const badgeVariants = cva( variant: { default: "border-transparent bg-primary text-primary-foreground", - outlinePrimary: "border-transparent bg-transparent border-primary text-primary", + outlinePrimary: + "border-transparent bg-transparent border-primary text-primary", secondary: "border-transparent bg-secondary text-secondary-foreground", destructive: @@ -18,12 +19,12 @@ const badgeVariants = cva( outline: "text-foreground", green: "border-green-600 bg-green-500/20 text-green-700 dark:text-green-300", yellow: "border-yellow-600 bg-yellow-500/20 text-yellow-700 dark:text-yellow-300", - red: "border-red-400 bg-red-300/20 text-red-600 dark:text-red-300", - }, + red: "border-red-400 bg-red-300/20 text-red-600 dark:text-red-300" + } }, defaultVariants: { - variant: "default", - }, + variant: "default" + } } ); diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 00000000..e7fdc353 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,99 @@ +import { + useState, + useEffect, + useCallback, + Dispatch, + SetStateAction +} from "react"; + +type SetValue = Dispatch>; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, SetValue] { + // Get initial value from localStorage or use the provided initial value + const readValue = useCallback((): T => { + // Prevent build error "window is undefined" during SSR + if (typeof window === "undefined") { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? (JSON.parse(item) as T) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }, [initialValue, key]); + + // State to store our value + const [storedValue, setStoredValue] = useState(readValue); + + // Return a wrapped version of useState's setter function that + // persists the new value to localStorage + const setValue: SetValue = useCallback( + (value) => { + // Prevent build error "window is undefined" during SSR + if (typeof window === "undefined") { + console.warn( + `Tried setting localStorage key "${key}" even though environment is not a client` + ); + } + + try { + // Allow value to be a function so we have the same API as useState + const newValue = + value instanceof Function ? value(storedValue) : value; + + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(newValue)); + + // Save state + setStoredValue(newValue); + + // Dispatch a custom event so every useLocalStorage hook is notified + window.dispatchEvent(new Event("local-storage")); + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }, + [key, storedValue] + ); + + // Listen for changes to this key from other tabs/windows + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key && e.newValue !== null) { + try { + setStoredValue(JSON.parse(e.newValue)); + } catch (error) { + console.warn( + `Error parsing localStorage value for key "${key}":`, + error + ); + } + } + }; + + // Listen for storage events (changes from other tabs) + window.addEventListener("storage", handleStorageChange); + + // Listen for custom event (changes from same tab) + const handleLocalStorageChange = () => { + setStoredValue(readValue()); + }; + window.addEventListener("local-storage", handleLocalStorageChange); + + return () => { + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener( + "local-storage", + handleLocalStorageChange + ); + }; + }, [key, readValue]); + + return [storedValue, setValue]; +} diff --git a/src/hooks/useRemoteExitNodeContext.ts b/src/hooks/useRemoteExitNodeContext.ts index 486147c4..6fe244c8 100644 --- a/src/hooks/useRemoteExitNodeContext.ts +++ b/src/hooks/useRemoteExitNodeContext.ts @@ -2,11 +2,15 @@ import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext"; import { build } from "@server/build"; +import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { useContext } from "react"; export function useRemoteExitNodeContext() { if (build == "oss") { - return null; + return { + remoteExitNode: {} as GetRemoteExitNodeResponse, + updateRemoteExitNode: () => {}, + }; } const context = useContext(RemoteExitNodeContext); if (context === undefined) { diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 5cef9f0e..14735053 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -51,6 +51,21 @@ export const internal = axios.create({ } }); +const remoteAPIURL = + process.env.NODE_ENV === "development" + ? (process.env.NEXT_PUBLIC_FOSSORIAL_REMOTE_API_URL ?? + "https://api.fossorial.io") + : "https://api.fossorial.io"; + +export const remote = axios.create({ + baseURL: `${remoteAPIURL}/api/v1`, + timeout: 10000, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": "x-csrf-protection" + } +}); + export const priv = axios.create({ baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, timeout: 10000, @@ -60,4 +75,3 @@ export const priv = axios.create({ }); export * from "./formatAxiosError"; - diff --git a/src/lib/durationToMs.ts b/src/lib/durationToMs.ts new file mode 100644 index 00000000..172bae15 --- /dev/null +++ b/src/lib/durationToMs.ts @@ -0,0 +1,13 @@ +export function durationToMs( + value: number, + unit: "seconds" | "minutes" | "hours" | "days" | "weeks" +): number { + const multipliers = { + seconds: 1000, + minutes: 60 * 1000, + hours: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000, + weeks: 7 * 24 * 60 * 60 * 1000 + }; + return value * multipliers[unit]; +} diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index 890a6e5f..f9349eef 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -21,7 +21,17 @@ export function pullEnv(): Env { environment: process.env.ENVIRONMENT as string, sandbox_mode: process.env.SANDBOX_MODE === "true" ? true : false, version: process.env.APP_VERSION as string, - dashboardUrl: process.env.DASHBOARD_URL as string + dashboardUrl: process.env.DASHBOARD_URL as string, + notifications: { + product_updates: + process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED === "true" + ? true + : false, + new_releases: + process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true" + ? true + : false + } }, email: { emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false diff --git a/src/lib/queries.ts b/src/lib/queries.ts new file mode 100644 index 00000000..3ddf32bf --- /dev/null +++ b/src/lib/queries.ts @@ -0,0 +1,67 @@ +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; +import { durationToMs } from "./durationToMs"; +import { build } from "@server/build"; +import { remote } from "./api"; +import type ResponseT from "@server/types/Response"; + +export type ProductUpdate = { + link: string | null; + build: "enterprise" | "oss" | "saas" | null; + id: number; + type: "Update" | "Important" | "New" | "Warning"; + title: string; + contents: string; + publishedAt: Date; + showUntil: Date; +}; + +export type LatestVersionResponse = { + pangolin: { + latestVersion: string; + releaseNotes: string; + }; +}; + +export const productUpdatesQueries = { + list: (enabled: boolean) => + queryOptions({ + queryKey: ["PRODUCT_UPDATES"] as const, + queryFn: async ({ signal }) => { + const sp = new URLSearchParams({ + build + }); + const data = await remote.get>( + `/product-updates?${sp.toString()}`, + { signal } + ); + return data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(5, "minutes"); + } + return false; + }, + enabled + }), + latestVersion: (enabled: boolean) => + queryOptions({ + queryKey: ["LATEST_VERSION"] as const, + queryFn: async ({ signal }) => { + const data = await remote.get>( + "/versions", + { signal } + ); + return data.data; + }, + placeholderData: keepPreviousData, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "minutes"); + } + return false; + }, + enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version + // because we don't need to listen for new versions there + }) +}; diff --git a/src/lib/timeAgoFormatter.ts b/src/lib/timeAgoFormatter.ts new file mode 100644 index 00000000..f6ae0175 --- /dev/null +++ b/src/lib/timeAgoFormatter.ts @@ -0,0 +1,47 @@ +export function timeAgoFormatter( + dateInput: string | Date, + short: boolean = false +): string { + const date = new Date(dateInput); + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + const secondsInMinute = 60; + const secondsInHour = 60 * secondsInMinute; + const secondsInDay = 24 * secondsInHour; + const secondsInWeek = 7 * secondsInDay; + const secondsInMonth = 30 * secondsInDay; + const secondsInYear = 365 * secondsInDay; + + let value: number; + let unit: Intl.RelativeTimeFormatUnit; + + if (diffInSeconds < secondsInMinute) { + value = diffInSeconds; + unit = "second"; + } else if (diffInSeconds < secondsInHour) { + value = Math.floor(diffInSeconds / secondsInMinute); + unit = "minute"; + } else if (diffInSeconds < secondsInDay) { + value = Math.floor(diffInSeconds / secondsInHour); + unit = "hour"; + } else if (diffInSeconds < secondsInWeek) { + value = Math.floor(diffInSeconds / secondsInDay); + unit = "day"; + } else if (diffInSeconds < secondsInMonth) { + value = Math.floor(diffInSeconds / secondsInWeek); + unit = "week"; + } else if (diffInSeconds < secondsInYear) { + value = Math.floor(diffInSeconds / secondsInMonth); + unit = "month"; + } else { + value = Math.floor(diffInSeconds / secondsInYear); + unit = "year"; + } + + const rtf = new Intl.RelativeTimeFormat(navigator.languages[0] ?? "en", { + numeric: "auto", + style: short ? "narrow" : "long" + }); + return rtf.format(-value, unit); +} diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index 9e519671..0cc22b97 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -4,6 +4,10 @@ export type Env = { sandbox_mode: boolean; version: string; dashboardUrl: string; + notifications: { + product_updates: boolean; + new_releases: boolean; + }; }; server: { externalPort: string; @@ -29,7 +33,7 @@ export type Env = { enableClients: boolean; hideSupporterKey: boolean; usePangolinDns: boolean; - }, + }; branding: { appName?: string; background_image_path?: string; @@ -44,22 +48,22 @@ export type Env = { navbar?: { width?: number; height?: number; - } - }, - loginPage?: { + }; + }; + loginPage: { titleText?: string; subtitleText?: string; - }, - signupPage?: { + }; + signupPage: { titleText?: string; subtitleText?: string; - }, - resourceAuthPage?: { + }; + resourceAuthPage: { showLogo?: boolean; hidePoweredBy?: boolean; titleText?: string; subtitleText?: string; - }, + }; footer?: string; }; };