Merge branch 'dev' into refactor/save-button-positions

This commit is contained in:
Fred KISSIE
2025-12-11 23:35:00 +01:00
37 changed files with 465 additions and 316 deletions

View File

@@ -41,7 +41,7 @@
</strong>
</p>
Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
## Installation
@@ -60,14 +60,20 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and contex
## Key Features
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
| <img width=500 /> | <img width=500 /> |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
| **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
## Download Clients
Download the Pangolin client for your platform:
- [Mac](https://pangolin.net/downloads/mac)
- [Windows](https://pangolin.net/downloads/windows)
- [Linux](https://pangolin.net/downloads/linux)
## Get Started

View File

@@ -1043,7 +1043,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",
@@ -1102,7 +1102,7 @@
"actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen",
"actionListIdpOrgs": "IDP-Organisationen auflisten",
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
"actionCreateClient": "Endgerät anlegen",
"actionCreateClient": "Client erstellen",
"actionDeleteClient": "Client löschen",
"actionUpdateClient": "Client aktualisieren",
"actionListClients": "Clients auflisten",
@@ -1201,24 +1201,24 @@
"sidebarLogsAnalytics": "Analytik",
"blueprints": "Baupläne",
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
"blueprintAdd": "Blaupause hinzufügen",
"blueprintGoBack": "Alle Blaupausen ansehen",
"blueprintCreate": "Blaupause erstellen",
"blueprintCreateDescription2": "Folge den Schritten unten, um eine neue Blaupause zu erstellen und anzuwenden",
"blueprintDetails": "Blaupausendetails",
"blueprintDetailsDescription": "Siehe das Ergebnis der angewendeten Blaupause und alle aufgetretenen Fehler",
"blueprintInfo": "Blaupauseninformation",
"blueprintAdd": "Blueprint hinzufügen",
"blueprintGoBack": "Alle Blueprints ansehen",
"blueprintCreate": "Blueprint erstellen",
"blueprintCreateDescription2": "Folge den unten aufgeführten Schritten, um einen neuen Blueprint zu erstellen und anzuwenden",
"blueprintDetails": "Blueprint Detailinformationen",
"blueprintDetailsDescription": "Siehe das Ergebnis des angewendeten Blueprints und alle aufgetretenen Fehler",
"blueprintInfo": "Blueprint Informationen",
"message": "Nachricht",
"blueprintContentsDescription": "Den YAML-Inhalt definieren, der die 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",
"parsedContents": "Analysierte Inhalte (Nur lesen)",
"enableDockerSocket": "Docker Blaupause aktivieren",
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
"enableDockerSocket": "Docker Blueprint aktivieren",
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blueprintbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
"enableDockerSocketLink": "Mehr erfahren",
"viewDockerContainers": "Docker Container anzeigen",
"containersIn": "Container in {siteName}",
@@ -1543,7 +1543,7 @@
"healthCheckPathRequired": "Gesundheits-Check-Pfad ist erforderlich",
"healthCheckMethodRequired": "HTTP-Methode ist erforderlich",
"healthCheckIntervalMin": "Prüfintervall muss mindestens 5 Sekunden betragen",
"healthCheckTimeoutMin": "Timeout muss mindestens 1 Sekunde betragen",
"healthCheckTimeoutMin": "Zeitüberschreitung muss mindestens 1 Sekunde betragen",
"healthCheckRetryMin": "Wiederholungsversuche müssen mindestens 1 betragen",
"httpMethod": "HTTP-Methode",
"selectHttpMethod": "HTTP-Methode auswählen",

View File

@@ -2070,6 +2070,8 @@
"timestamp": "Timestamp",
"accessLogs": "Access Logs",
"exportCsv": "Export CSV",
"exportError": "Unknown error when exporting CSV",
"exportCsvTooltip": "Within Time Range",
"actorId": "Actor ID",
"allowedByRule": "Allowed by Rule",
"allowedNoAuth": "Allowed No Auth",

177
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "SEE LICENSE IN LICENSE AND README.md",
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.2.0",
"@aws-sdk/client-s3": "3.947.0",
"@aws-sdk/client-s3": "3.948.0",
"@faker-js/faker": "10.1.0",
"@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2",
@@ -72,7 +72,7 @@
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.556.0",
"lucide-react": "0.559.0",
"maxmind": "5.0.1",
"moment": "2.30.1",
"next": "15.5.7",
@@ -82,7 +82,7 @@
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.11",
"npm": "11.6.4",
"npm": "11.7.0",
"nprogress": "0.2.0",
"oslo": "1.2.1",
"pg": "8.16.3",
@@ -97,7 +97,7 @@
"rebuild": "0.1.2",
"recharts": "2.15.4",
"reodotdev": "1.0.0",
"resend": "6.5.2",
"resend": "6.6.0",
"semver": "7.7.3",
"stripe": "20.0.0",
"swagger-ui-express": "5.0.1",
@@ -133,7 +133,7 @@
"@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/nprogress": "0.2.3",
"@types/pg": "8.15.6",
"@types/pg": "8.16.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
@@ -147,7 +147,7 @@
"esbuild-node-externals": "1.20.1",
"postcss": "8.5.6",
"prettier": "3.7.4",
"react-email": "5.0.6",
"react-email": "5.0.7",
"tailwindcss": "4.1.17",
"tsc-alias": "1.8.16",
"tsx": "4.21.0",
@@ -396,23 +396,23 @@
}
},
"node_modules/@aws-sdk/client-s3": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.947.0.tgz",
"integrity": "sha512-ICgnI8D3ccIX9alsLksPFY2bX5CAIbyB+q19sXJgPhzCJ5kWeQ6LQ5xBmRVT5kccmsVGbbJdhnLXHyiN5LZsWg==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.948.0.tgz",
"integrity": "sha512-uvEjds8aYA9SzhBS8RKDtsDUhNV9VhqKiHTcmvhM7gJO92q0WTn8/QeFTdNyLc6RxpiDyz+uBxS7PcdNiZzqfA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.947.0",
"@aws-sdk/credential-provider-node": "3.947.0",
"@aws-sdk/credential-provider-node": "3.948.0",
"@aws-sdk/middleware-bucket-endpoint": "3.936.0",
"@aws-sdk/middleware-expect-continue": "3.936.0",
"@aws-sdk/middleware-flexible-checksums": "3.947.0",
"@aws-sdk/middleware-host-header": "3.936.0",
"@aws-sdk/middleware-location-constraint": "3.936.0",
"@aws-sdk/middleware-logger": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.948.0",
"@aws-sdk/middleware-sdk-s3": "3.947.0",
"@aws-sdk/middleware-ssec": "3.936.0",
"@aws-sdk/middleware-user-agent": "3.947.0",
@@ -462,9 +462,9 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.947.0.tgz",
"integrity": "sha512-sDwcO8SP290WSErY1S8pz8hTafeghKmmWjNVks86jDK30wx62CfazOTeU70IpWgrUBEygyXk/zPogHsUMbW2Rg==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz",
"integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -472,7 +472,7 @@
"@aws-sdk/core": "3.947.0",
"@aws-sdk/middleware-host-header": "3.936.0",
"@aws-sdk/middleware-logger": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.948.0",
"@aws-sdk/middleware-user-agent": "3.947.0",
"@aws-sdk/region-config-resolver": "3.936.0",
"@aws-sdk/types": "3.936.0",
@@ -572,19 +572,19 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.947.0.tgz",
"integrity": "sha512-A2ZUgJUJZERjSzvCi2NR/hBVbVkTXPD0SdKcR/aITb30XwF+n3T963b+pJl90qhOspoy7h0IVYNR7u5Nr9tJdQ==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz",
"integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.947.0",
"@aws-sdk/credential-provider-env": "3.947.0",
"@aws-sdk/credential-provider-http": "3.947.0",
"@aws-sdk/credential-provider-login": "3.947.0",
"@aws-sdk/credential-provider-login": "3.948.0",
"@aws-sdk/credential-provider-process": "3.947.0",
"@aws-sdk/credential-provider-sso": "3.947.0",
"@aws-sdk/credential-provider-web-identity": "3.947.0",
"@aws-sdk/nested-clients": "3.947.0",
"@aws-sdk/credential-provider-sso": "3.948.0",
"@aws-sdk/credential-provider-web-identity": "3.948.0",
"@aws-sdk/nested-clients": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/credential-provider-imds": "^4.2.5",
"@smithy/property-provider": "^4.2.5",
@@ -597,13 +597,13 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-login": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.947.0.tgz",
"integrity": "sha512-u7M3hazcB7aJiVwosNdJRbIJDzbwQ861NTtl6S0HmvWpixaVb7iyhJZWg8/plyUznboZGBm7JVEdxtxv3u0bTA==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz",
"integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.947.0",
"@aws-sdk/nested-clients": "3.947.0",
"@aws-sdk/nested-clients": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/property-provider": "^4.2.5",
"@smithy/protocol-http": "^5.3.5",
@@ -616,17 +616,17 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.947.0.tgz",
"integrity": "sha512-S0Zqebr71KyrT6J4uYPhwV65g4V5uDPHnd7dt2W34FcyPu+hVC7Hx4MFmsPyVLeT5cMCkkZvmY3kAoEzgUPJJg==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz",
"integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "3.947.0",
"@aws-sdk/credential-provider-http": "3.947.0",
"@aws-sdk/credential-provider-ini": "3.947.0",
"@aws-sdk/credential-provider-ini": "3.948.0",
"@aws-sdk/credential-provider-process": "3.947.0",
"@aws-sdk/credential-provider-sso": "3.947.0",
"@aws-sdk/credential-provider-web-identity": "3.947.0",
"@aws-sdk/credential-provider-sso": "3.948.0",
"@aws-sdk/credential-provider-web-identity": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/credential-provider-imds": "^4.2.5",
"@smithy/property-provider": "^4.2.5",
@@ -656,14 +656,14 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.947.0.tgz",
"integrity": "sha512-NktnVHTGaUMaozxycYrepvb3yfFquHTQ53lt6hBEVjYBzK3C4tVz0siUpr+5RMGLSiZ5bLBp2UjJPgwx4i4waQ==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz",
"integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/client-sso": "3.947.0",
"@aws-sdk/client-sso": "3.948.0",
"@aws-sdk/core": "3.947.0",
"@aws-sdk/token-providers": "3.947.0",
"@aws-sdk/token-providers": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/property-provider": "^4.2.5",
"@smithy/shared-ini-file-loader": "^4.4.0",
@@ -675,13 +675,13 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.947.0.tgz",
"integrity": "sha512-gokm/e/YHiHLrZgLq4j8tNAn8RJDPbIcglFRKgy08q8DmAqHQ8MXAKW3eS0QjAuRXU9mcMmUo1NrX6FRNBCCPw==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz",
"integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.947.0",
"@aws-sdk/nested-clients": "3.947.0",
"@aws-sdk/nested-clients": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/property-provider": "^4.2.5",
"@smithy/shared-ini-file-loader": "^4.4.0",
@@ -692,6 +692,22 @@
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": {
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz",
"integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "3.936.0",
"@aws/lambda-invoke-store": "^0.2.2",
"@smithy/protocol-http": "^5.3.5",
"@smithy/types": "^4.9.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.947.0.tgz",
@@ -736,9 +752,9 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.947.0.tgz",
"integrity": "sha512-DjRJEYNnHUTu9kGPPQDTSXquwSEd6myKR4ssI4FaYLFhdT3ldWpj73yYt807H3tdmhS7vPmdVqchSJnjurUQAw==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz",
"integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -746,7 +762,7 @@
"@aws-sdk/core": "3.947.0",
"@aws-sdk/middleware-host-header": "3.936.0",
"@aws-sdk/middleware-logger": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.936.0",
"@aws-sdk/middleware-recursion-detection": "3.948.0",
"@aws-sdk/middleware-user-agent": "3.947.0",
"@aws-sdk/region-config-resolver": "3.936.0",
"@aws-sdk/types": "3.936.0",
@@ -802,13 +818,13 @@
}
},
"node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": {
"version": "3.947.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.947.0.tgz",
"integrity": "sha512-X/DyB8GuK44rsE89Tn5+s542B3PhGbXQSgV8lvqHDzvicwCt0tWny6790st6CPETrVVV2K3oJMfG5U3/jAmaZA==",
"version": "3.948.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz",
"integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.947.0",
"@aws-sdk/nested-clients": "3.947.0",
"@aws-sdk/nested-clients": "3.948.0",
"@aws-sdk/types": "3.936.0",
"@smithy/property-provider": "^4.2.5",
"@smithy/shared-ini-file-loader": "^4.4.0",
@@ -1264,6 +1280,7 @@
"version": "3.936.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz",
"integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "3.936.0",
@@ -9360,9 +9377,9 @@
"license": "MIT"
},
"node_modules/@types/pg": {
"version": "8.15.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
"integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
@@ -15915,9 +15932,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.556.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.556.0.tgz",
"integrity": "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==",
"version": "0.559.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.559.0.tgz",
"integrity": "sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -16515,9 +16532,9 @@
}
},
"node_modules/npm": {
"version": "11.6.4",
"resolved": "https://registry.npmjs.org/npm/-/npm-11.6.4.tgz",
"integrity": "sha512-ERjKtGoFpQrua/9bG0+h3xiv/4nVdGViCjUYA1AmlV24fFvfnSB7B7dIfZnySQ1FDLd0ZVrWPsLLp78dCtJdRQ==",
"version": "11.7.0",
"resolved": "https://registry.npmjs.org/npm/-/npm-11.7.0.tgz",
"integrity": "sha512-wiCZpv/41bIobCoJ31NStIWKfAxxYyD1iYnWCtiyns8s5v3+l8y0HCP/sScuH6B5+GhIfda4HQKiqeGZwJWhFw==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -16596,8 +16613,8 @@
],
"dependencies": {
"@isaacs/string-locale-compare": "^1.1.0",
"@npmcli/arborist": "^9.1.8",
"@npmcli/config": "^10.4.4",
"@npmcli/arborist": "^9.1.9",
"@npmcli/config": "^10.4.5",
"@npmcli/fs": "^5.0.0",
"@npmcli/map-workspaces": "^5.0.3",
"@npmcli/metavuln-calculator": "^9.0.3",
@@ -16622,11 +16639,11 @@
"is-cidr": "^6.0.1",
"json-parse-even-better-errors": "^5.0.0",
"libnpmaccess": "^10.0.3",
"libnpmdiff": "^8.0.11",
"libnpmexec": "^10.1.10",
"libnpmfund": "^7.0.11",
"libnpmdiff": "^8.0.12",
"libnpmexec": "^10.1.11",
"libnpmfund": "^7.0.12",
"libnpmorg": "^8.0.1",
"libnpmpack": "^9.0.11",
"libnpmpack": "^9.0.12",
"libnpmpublish": "^11.1.3",
"libnpmsearch": "^9.0.1",
"libnpmteam": "^8.0.2",
@@ -16734,7 +16751,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/arborist": {
"version": "9.1.8",
"version": "9.1.9",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -16780,7 +16797,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/config": {
"version": "10.4.4",
"version": "10.4.5",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -17518,11 +17535,11 @@
}
},
"node_modules/npm/node_modules/libnpmdiff": {
"version": "8.0.11",
"version": "8.0.12",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/arborist": "^9.1.8",
"@npmcli/arborist": "^9.1.9",
"@npmcli/installed-package-contents": "^4.0.0",
"binary-extensions": "^3.0.0",
"diff": "^8.0.2",
@@ -17536,11 +17553,11 @@
}
},
"node_modules/npm/node_modules/libnpmexec": {
"version": "10.1.10",
"version": "10.1.11",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/arborist": "^9.1.8",
"@npmcli/arborist": "^9.1.9",
"@npmcli/package-json": "^7.0.0",
"@npmcli/run-script": "^10.0.0",
"ci-info": "^4.0.0",
@@ -17558,11 +17575,11 @@
}
},
"node_modules/npm/node_modules/libnpmfund": {
"version": "7.0.11",
"version": "7.0.12",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/arborist": "^9.1.8"
"@npmcli/arborist": "^9.1.9"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -17581,11 +17598,11 @@
}
},
"node_modules/npm/node_modules/libnpmpack": {
"version": "9.0.11",
"version": "9.0.12",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/arborist": "^9.1.8",
"@npmcli/arborist": "^9.1.9",
"@npmcli/run-script": "^10.0.0",
"npm-package-arg": "^13.0.0",
"pacote": "^21.0.2"
@@ -19780,9 +19797,9 @@
}
},
"node_modules/react-email": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz",
"integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.7.tgz",
"integrity": "sha512-JsWzxl3O82Gw9HRRNJm8VjQLB8c7R5TGbP89Ffj+/Qdb2H2N4J0XRXkhqiucMvmucuqNqe9mNndZkh3jh638xA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20871,9 +20888,9 @@
"license": "MIT"
},
"node_modules/resend": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.5.2.tgz",
"integrity": "sha512-Yl83UvS8sYsjgmF8dVbNPzlfpmb3DkLUk3VwsAbkaEFo9UMswpNuPGryHBXGk+Ta4uYMv5HmjVk3j9jmNkcEDg==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.6.0.tgz",
"integrity": "sha512-d1WoOqSxj5x76JtQMrieNAG1kZkh4NU4f+Je1yq4++JsDpLddhEwnJlNfvkCzvUuZy9ZquWmMMAm2mENd2JvRw==",
"license": "MIT",
"dependencies": {
"svix": "1.76.1"

View File

@@ -34,7 +34,7 @@
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.2.0",
"@aws-sdk/client-s3": "3.947.0",
"@aws-sdk/client-s3": "3.948.0",
"@faker-js/faker": "10.1.0",
"@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2",
@@ -96,7 +96,7 @@
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.556.0",
"lucide-react": "0.559.0",
"maxmind": "5.0.1",
"moment": "2.30.1",
"next": "15.5.7",
@@ -106,7 +106,7 @@
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.11",
"npm": "11.6.4",
"npm": "11.7.0",
"nprogress": "0.2.0",
"oslo": "1.2.1",
"pg": "8.16.3",
@@ -121,7 +121,7 @@
"rebuild": "0.1.2",
"recharts": "2.15.4",
"reodotdev": "1.0.0",
"resend": "6.5.2",
"resend": "6.6.0",
"semver": "7.7.3",
"stripe": "20.0.0",
"swagger-ui-express": "5.0.1",
@@ -156,7 +156,7 @@
"@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/nprogress": "0.2.3",
"@types/pg": "8.15.6",
"@types/pg": "8.16.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
@@ -171,7 +171,7 @@
"esbuild-node-externals": "1.20.1",
"postcss": "8.5.6",
"prettier": "3.7.4",
"react-email": "5.0.6",
"react-email": "5.0.7",
"tailwindcss": "4.1.17",
"tsc-alias": "1.8.16",
"tsx": "4.21.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -2,7 +2,8 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.13.0-rc.0";
// export const APP_VERSION = "1.13.0-rc.0";
export const APP_VERSION = "1.13.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -1085,7 +1085,7 @@ async function handleMessagesForClientSites(
continue;
}
await holepunchSiteAdd(
await initPeerAddHandshake(
// this will kick off the add peer process for the client
client.clientId,
{

View File

@@ -2,9 +2,9 @@ import { PostHog } from "posthog-node";
import config from "./config";
import { getHostMeta } from "./hostMeta";
import logger from "@server/logger";
import { apiKeys, db, roles } from "@server/db";
import { apiKeys, db, roles, siteResources } from "@server/db";
import { sites, users, orgs, resources, clients, idp } from "@server/db";
import { eq, count, notInArray, and } from "drizzle-orm";
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
import { APP_VERSION } from "./consts";
import crypto from "crypto";
import { UserType } from "@server/types/UserTypes";
@@ -25,7 +25,7 @@ class TelemetryClient {
return;
}
if (build !== "oss") {
if (build === "saas") {
return;
}
@@ -41,14 +41,18 @@ class TelemetryClient {
this.client?.shutdown();
});
this.sendStartupEvents().catch((err) => {
logger.error("Failed to send startup telemetry:", err);
});
this.sendStartupEvents()
.catch((err) => {
logger.error("Failed to send startup telemetry:", err);
})
.then(() => {
logger.debug("Successfully sent startup telemetry data");
});
this.startAnalyticsInterval();
logger.info(
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry"
"Pangolin gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry"
);
} else if (!this.enabled) {
logger.info(
@@ -60,9 +64,13 @@ class TelemetryClient {
private startAnalyticsInterval() {
this.intervalId = setInterval(
() => {
this.collectAndSendAnalytics().catch((err) => {
logger.error("Failed to collect analytics:", err);
});
this.collectAndSendAnalytics()
.catch((err) => {
logger.error("Failed to collect analytics:", err);
})
.then(() => {
logger.debug("Successfully sent analytics data");
});
},
48 * 60 * 60 * 1000
);
@@ -99,9 +107,14 @@ class TelemetryClient {
const [resourcesCount] = await db
.select({ count: count() })
.from(resources);
const [clientsCount] = await db
const [userDevicesCount] = await db
.select({ count: count() })
.from(clients);
.from(clients)
.where(isNotNull(clients.userId));
const [machineClients] = await db
.select({ count: count() })
.from(clients)
.where(isNull(clients.userId));
const [idpCount] = await db.select({ count: count() }).from(idp);
const [onlineSitesCount] = await db
.select({ count: count() })
@@ -146,6 +159,24 @@ class TelemetryClient {
const supporterKey = config.getSupporterData();
const allPrivateResources = await db.select().from(siteResources);
const numPrivResources = allPrivateResources.length;
let numPrivResourceAliases = 0;
let numPrivResourceHosts = 0;
let numPrivResourceCidr = 0;
for (const res of allPrivateResources) {
if (res.mode === "host") {
numPrivResourceHosts += 1;
} else if (res.mode === "cidr") {
numPrivResourceCidr += 1;
}
if (res.alias) {
numPrivResourceAliases += 1;
}
}
return {
numSites: sitesCount.count,
numUsers: usersCount.count,
@@ -153,7 +184,11 @@ class TelemetryClient {
numUsersOidc: usersOidcCount.count,
numOrganizations: orgsCount.count,
numResources: resourcesCount.count,
numClients: clientsCount.count,
numPrivateResources: numPrivResources,
numPrivateResourceAliases: numPrivResourceAliases,
numPrivateResourceHosts: numPrivResourceHosts,
numUserDevices: userDevicesCount.count,
numMachineClients: machineClients.count,
numIdentityProviders: idpCount.count,
numSitesOnline: onlineSitesCount.count,
resources: resourceDetails,
@@ -196,7 +231,7 @@ class TelemetryClient {
logger.debug("Sending enterprise startup telemetry payload:", {
payload
});
// this.client.capture(payload);
this.client.capture(payload);
}
if (build === "oss") {
@@ -246,7 +281,12 @@ class TelemetryClient {
num_users_oidc: stats.numUsersOidc,
num_organizations: stats.numOrganizations,
num_resources: stats.numResources,
num_clients: stats.numClients,
num_private_resources: stats.numPrivateResources,
num_private_resource_aliases:
stats.numPrivateResourceAliases,
num_private_resource_hosts: stats.numPrivateResourceHosts,
num_user_devices: stats.numUserDevices,
num_machine_clients: stats.numMachineClients,
num_identity_providers: stats.numIdentityProviders,
num_sites_online: stats.numSitesOnline,
num_resources_sso_enabled: stats.resources.filter(

View File

@@ -22,9 +22,11 @@ import logger from "@server/logger";
import {
queryAccessAuditLogsParams,
queryAccessAuditLogsQuery,
queryAccess
queryAccess,
countAccessQuery
} from "./queryAccessAuditLog";
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs";
registry.registerPath({
method: "get",
@@ -65,6 +67,15 @@ export async function exportAccessAuditLogs(
}
const data = { ...parsedQuery.data, ...parsedParams.data };
const [{ count }] = await countAccessQuery(data);
if (count > MAX_EXPORT_LIMIT) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is ${MAX_EXPORT_LIMIT} rows. Please select a shorter time range to reduce the data.`
)
);
}
const baseQuery = queryAccess(data);

View File

@@ -22,9 +22,11 @@ import logger from "@server/logger";
import {
queryActionAuditLogsParams,
queryActionAuditLogsQuery,
queryAction
queryAction,
countActionQuery
} from "./queryActionAuditLog";
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs";
registry.registerPath({
method: "get",
@@ -65,6 +67,15 @@ export async function exportActionAuditLogs(
}
const data = { ...parsedQuery.data, ...parsedParams.data };
const [{ count }] = await countActionQuery(data);
if (count > MAX_EXPORT_LIMIT) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is ${MAX_EXPORT_LIMIT} rows. Please select a shorter time range to reduce the data.`
)
);
}
const baseQuery = queryAction(data);

View File

@@ -24,6 +24,7 @@ import { fromError } from "zod-validation-error";
import { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date
@@ -32,7 +33,14 @@ export const queryAccessAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"Start time as ISO date string (defaults to 7 days ago)"
}),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -24,6 +24,7 @@ import { fromError } from "zod-validation-error";
import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryActionAuditLogsQuery = z.object({
// iso string just validate its a parseable date
@@ -32,7 +33,14 @@ export const queryActionAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"Start time as ISO date string (defaults to 7 days ago)"
}),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -9,17 +9,23 @@ import logger from "@server/logger";
import {
queryAccessAuditLogsQuery,
queryRequestAuditLogsParams,
queryRequest
queryRequest,
countRequestQuery
} from "./queryRequestAuditLog";
import { generateCSV } from "./generateCSV";
export const MAX_EXPORT_LIMIT = 50_000;
registry.registerPath({
method: "get",
path: "/org/{orgId}/logs/request",
description: "Query the request audit log for an organization",
tags: [OpenAPITags.Org],
request: {
query: queryAccessAuditLogsQuery,
query: queryAccessAuditLogsQuery.omit({
limit: true,
offset: true
}),
params: queryRequestAuditLogsParams
},
responses: {}
@@ -53,9 +59,19 @@ export async function exportRequestAuditLogs(
const data = { ...parsedQuery.data, ...parsedParams.data };
const [{ count }] = await countRequestQuery(data);
if (count > MAX_EXPORT_LIMIT) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is ${MAX_EXPORT_LIMIT} rows. Please select a shorter time range to reduce the data.`
)
);
}
const baseQuery = queryRequest(data);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const log = await baseQuery.limit(MAX_EXPORT_LIMIT);
const csvData = generateCSV(log);

View File

@@ -2,7 +2,7 @@ import { db, requestAuditLog, driver } from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { eq, gt, lt, and, count, sql, desc, not, isNull } from "drizzle-orm";
import { eq, gte, lte, and, count, sql, desc, not, isNull } from "drizzle-orm";
import { OpenAPITags } from "@server/openApi";
import { z } from "zod";
import createHttpError from "http-errors";
@@ -10,6 +10,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date
@@ -19,7 +20,14 @@ const queryAccessAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional(),
.optional()
.prefault(() => getSevenDaysAgo().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"Start time as ISO date string (defaults to 7 days ago)"
}),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
@@ -55,15 +63,10 @@ type Q = z.infer<typeof queryRequestAuditLogsCombined>;
async function query(query: Q) {
let baseConditions = and(
eq(requestAuditLog.orgId, query.orgId),
lt(requestAuditLog.timestamp, query.timeEnd)
gte(requestAuditLog.timestamp, query.timeStart),
lte(requestAuditLog.timestamp, query.timeEnd)
);
if (query.timeStart) {
baseConditions = and(
baseConditions,
gt(requestAuditLog.timestamp, query.timeStart)
);
}
if (query.resourceId) {
baseConditions = and(
baseConditions,

View File

@@ -11,6 +11,7 @@ import { fromError } from "zod-validation-error";
import { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date
@@ -19,7 +20,14 @@ export const queryAccessAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"Start time as ISO date string (defaults to 7 days ago)"
}),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -31,7 +31,12 @@ import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsFor
const createOrgSchema = z.strictObject({
orgId: z.string(),
name: z.string().min(1).max(255),
subnet: z.string()
subnet: z
// .union([z.cidrv4(), z.cidrv6()])
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
.refine((val) => isValidCIDR(val), {
message: "Invalid subnet CIDR"
})
});
registry.registerPath({
@@ -81,15 +86,6 @@ export async function createOrg(
const { orgId, name, subnet } = parsedBody.data;
if (!isValidCIDR(subnet)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid subnet format. Please provide a valid CIDR notation."
)
);
}
// TODO: for now we are making all of the orgs the same subnet
// make sure the subnet is unique
// const subnetExists = await db

View File

@@ -53,7 +53,8 @@ const createSiteResourceSchema = z
if (data.mode === "host") {
// Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z
.union([z.ipv4(), z.ipv6()])
// .union([z.ipv4(), z.ipv6()])
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
.safeParse(data.destination).success;
if (isValidIP) {
@@ -80,7 +81,8 @@ const createSiteResourceSchema = z
if (data.mode === "cidr") {
// Check if it's a valid CIDR (v4 or v6)
const isValidCIDR = z
.union([z.cidrv4(), z.cidrv6()])
// .union([z.cidrv4(), z.cidrv6()])
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
.safeParse(data.destination).success;
return isValidCIDR;
}

View File

@@ -62,7 +62,8 @@ const updateSiteResourceSchema = z
(data) => {
if (data.mode === "host" && data.destination) {
const isValidIP = z
.union([z.ipv4(), z.ipv6()])
// .union([z.ipv4(), z.ipv6()])
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
.safeParse(data.destination).success;
if (isValidIP) {
@@ -89,7 +90,8 @@ const updateSiteResourceSchema = z
if (data.mode === "cidr" && data.destination) {
// Check if it's a valid CIDR (v4 or v6)
const isValidCIDR = z
.union([z.cidrv4(), z.cidrv6()])
// .union([z.cidrv4(), z.cidrv6()])
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
.safeParse(data.destination).success;
return isValidCIDR;
}

View File

@@ -1,16 +1,12 @@
"use client";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useTransition } from "react";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import {
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { LogDataTable } from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { ArrowUpRight, Key, User } from "lucide-react";
@@ -21,21 +17,22 @@ import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusCo
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import axios from "axios";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams();
const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [isExporting, startTransition] = useTransition();
const [filterAttributes, setFilterAttributes] = useState<{
actors: string[];
resources: {
@@ -70,9 +67,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => {
return getStoredPageSize("access-audit-logs", 20);
});
const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
// Set default date range to last 24 hours
const getDefaultDateRange = () => {
@@ -91,11 +86,11 @@ export default function GeneralPage() {
}
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const lastWeek = getSevenDaysAgo();
return {
startDate: {
date: yesterday
date: lastWeek
},
endDate: {
date: now
@@ -148,7 +143,6 @@ export default function GeneralPage() {
// Handle page size changes
const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
setStoredPageSize(newPageSize, "access-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
};
@@ -309,8 +303,6 @@ export default function GeneralPage() {
const exportData = async () => {
try {
setIsExporting(true);
// Prepare query params for export
const params: any = {
timeStart: dateRange.startDate?.date
@@ -339,11 +331,21 @@ export default function GeneralPage() {
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
setIsExporting(false);
} catch (error) {
let apiErrorMessage: string | null = null;
if (axios.isAxiosError(error) && error.response) {
const data = error.response.data;
if (data instanceof Blob && data.type === "application/json") {
// Parse the Blob as JSON
const text = await data.text();
const errorData = JSON.parse(text);
apiErrorMessage = errorData.message;
}
}
toast({
title: t("error"),
description: t("exportError"),
description: apiErrorMessage ?? t("exportError"),
variant: "destructive"
});
}
@@ -631,7 +633,7 @@ export default function GeneralPage() {
title={t("accessLogs")}
onRefresh={refreshData}
isRefreshing={isRefreshing}
onExport={exportData}
onExport={() => startTransition(exportData)}
isExporting={isExporting}
onDateRangeChange={handleDateRangeChange}
dateRange={{

View File

@@ -1,32 +1,28 @@
"use client";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useState, useRef, useEffect } from "react";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import {
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { Key, User } from "lucide-react";
import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import { build } from "@server/build";
import { ColumnDef } from "@tanstack/react-table";
import axios from "axios";
import { Key, User } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams();
const searchParams = useSearchParams();
const subscription = useSubscriptionStatusContext();
@@ -34,7 +30,7 @@ export default function GeneralPage() {
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [isExporting, startTransition] = useTransition();
const [filterAttributes, setFilterAttributes] = useState<{
actors: string[];
actions: string[];
@@ -58,9 +54,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => {
return getStoredPageSize("action-audit-logs", 20);
});
const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
// Set default date range to last 24 hours
const getDefaultDateRange = () => {
@@ -79,11 +73,11 @@ export default function GeneralPage() {
}
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const lastWeek = getSevenDaysAgo();
return {
startDate: {
date: yesterday
date: lastWeek
},
endDate: {
date: now
@@ -136,7 +130,6 @@ export default function GeneralPage() {
// Handle page size changes
const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
setStoredPageSize(newPageSize, "action-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
};
@@ -293,8 +286,6 @@ export default function GeneralPage() {
const exportData = async () => {
try {
setIsExporting(true);
// Prepare query params for export
const params: any = {
timeStart: dateRange.startDate?.date
@@ -323,11 +314,21 @@ export default function GeneralPage() {
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
setIsExporting(false);
} catch (error) {
let apiErrorMessage: string | null = null;
if (axios.isAxiosError(error) && error.response) {
const data = error.response.data;
if (data instanceof Blob && data.type === "application/json") {
// Parse the Blob as JSON
const text = await data.text();
const errorData = JSON.parse(text);
apiErrorMessage = errorData.message;
}
}
toast({
title: t("error"),
description: t("exportError"),
description: apiErrorMessage ?? t("exportError"),
variant: "destructive"
});
}
@@ -484,7 +485,7 @@ export default function GeneralPage() {
searchColumn="action"
onRefresh={refreshData}
isRefreshing={isRefreshing}
onExport={exportData}
onExport={() => startTransition(exportData)}
isExporting={isExporting}
onDateRangeChange={handleDateRangeChange}
dateRange={{

View File

@@ -1,34 +1,32 @@
"use client";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useState, useRef, useEffect } from "react";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import {
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react";
import Link from "next/link";
import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { useTranslations } from "next-intl";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import { ColumnDef } from "@tanstack/react-table";
import axios from "axios";
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
export default function GeneralPage() {
const router = useRouter();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams();
const searchParams = useSearchParams();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [isExporting, startTransition] = useTransition();
// Pagination state
const [totalCount, setTotalCount] = useState<number>(0);
@@ -36,9 +34,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => {
return getStoredPageSize("request-audit-logs", 20);
});
const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
const [filterAttributes, setFilterAttributes] = useState<{
actors: string[];
@@ -95,11 +91,11 @@ export default function GeneralPage() {
}
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const lastWeek = getSevenDaysAgo();
return {
startDate: {
date: yesterday
date: lastWeek
},
endDate: {
date: now
@@ -152,7 +148,6 @@ export default function GeneralPage() {
// Handle page size changes
const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
setStoredPageSize(newPageSize, "request-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
};
@@ -302,8 +297,6 @@ export default function GeneralPage() {
const exportData = async () => {
try {
setIsExporting(true);
// Prepare query params for export
const params: any = {
timeStart: dateRange.startDate?.date
@@ -335,11 +328,21 @@ export default function GeneralPage() {
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
setIsExporting(false);
} catch (error) {
let apiErrorMessage: string | null = null;
if (axios.isAxiosError(error) && error.response) {
const data = error.response.data;
if (data instanceof Blob && data.type === "application/json") {
// Parse the Blob as JSON
const text = await data.text();
const errorData = JSON.parse(text);
apiErrorMessage = errorData.message;
}
}
toast({
title: t("error"),
description: t("exportError"),
description: apiErrorMessage ?? t("exportError"),
variant: "destructive"
});
}
@@ -773,7 +776,7 @@ export default function GeneralPage() {
searchColumn="host"
onRefresh={refreshData}
isRefreshing={isRefreshing}
onExport={exportData}
onExport={() => startTransition(exportData)}
isExporting={isExporting}
onDateRangeChange={handleDateRangeChange}
dateRange={{

View File

@@ -131,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
return (
<CredenzaHeader className={cn("-mx-6 px-6 pb-6 border-b border-border", className)} {...props}>
<CredenzaHeader className={cn("-mx-6 px-6", className)} {...props}>
{children}
</CredenzaHeader>
);

View File

@@ -1,22 +1,27 @@
"use client";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient } from "@app/lib/api";
import { cn } from "@app/lib/cn";
import {
logAnalyticsFiltersSchema,
logQueries,
resourceQueries
} from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Card, CardContent, CardHeader } from "./ui/card";
import { LoaderIcon, RefreshCw, XIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { DateRangePicker, type DateTimeValue } from "./DateTimePicker";
import { Button } from "./ui/button";
import { cn } from "@app/lib/cn";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader } from "./ui/card";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "./InfoSection";
import { Label } from "./ui/label";
import {
Select,
SelectContent,
@@ -24,23 +29,10 @@ import {
SelectTrigger,
SelectValue
} from "./ui/select";
import { Label } from "./ui/label";
import { Separator } from "./ui/separator";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "./InfoSection";
import { WorldMap } from "./WorldMap";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
ChartContainer,
ChartLegend,
@@ -49,7 +41,13 @@ import {
ChartTooltipContent,
type ChartConfig
} from "./ui/chart";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export type AnalyticsContentProps = {
orgId: string;
@@ -67,17 +65,18 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
const isEmptySearchParams =
!filters.resourceId && !filters.timeStart && !filters.timeEnd;
const env = useEnvContext();
const [api] = useState(() => createApiClient(env));
const router = useRouter();
console.log({ filters });
const dateRange = {
startDate: filters.timeStart ? new Date(filters.timeStart) : undefined,
endDate: filters.timeEnd ? new Date(filters.timeEnd) : undefined
startDate: filters.timeStart
? new Date(filters.timeStart)
: getSevenDaysAgo(),
endDate: filters.timeEnd ? new Date(filters.timeEnd) : new Date()
};
const { data: resources = [], isFetching: isFetchingResources } = useQuery(
resourceQueries.listNamesPerOrg(props.orgId, api)
resourceQueries.listNamesPerOrg(props.orgId)
);
const {
@@ -88,7 +87,6 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
} = useQuery(
logQueries.requestAnalytics({
orgId: props.orgId,
api,
filters
})
);

View File

@@ -1,16 +1,5 @@
"use client";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
SortingState,
getSortedRowModel,
ColumnFiltersState,
getFilteredRowModel
} from "@tanstack/react-table";
import {
Table,
TableBody,
@@ -19,29 +8,36 @@ import {
TableHeader,
TableRow
} from "@/components/ui/table";
import { Button } from "@app/components/ui/button";
import { useEffect, useMemo, useState } from "react";
import { Input } from "@app/components/ui/input";
import { DataTablePagination } from "@app/components/DataTablePagination";
import {
Plus,
Search,
RefreshCw,
Filter,
X,
Download,
ChevronRight,
ChevronDown
} from "lucide-react";
import {
Card,
CardContent,
CardHeader,
CardTitle
} from "@app/components/ui/card";
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
import { useTranslations } from "next-intl";
import { DateRangePicker, DateTimeValue } from "@app/components/DateTimePicker";
import { Button } from "@app/components/ui/button";
import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable
} from "@tanstack/react-table";
import {
ChevronDown,
ChevronRight,
Download,
Loader,
RefreshCw
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useState, useEffect, useMemo } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
const STORAGE_KEYS = {
PAGE_SIZE: "datatable-page-size",
@@ -400,15 +396,28 @@ export function LogDataTable<TData, TValue>({
</Button>
)}
{onExport && (
<Button
onClick={() => !disabled && onExport()}
disabled={isExporting || disabled}
>
<Download
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
/>
{t("exportCsv")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() =>
!disabled && onExport()
}
disabled={isExporting || disabled}
>
{isExporting ? (
<Loader className="mr-2 size-4 animate-spin" />
) : (
<Download className="mr-2 size-4" />
)}
{t("exportCsv")}
</Button>
</TooltipTrigger>
<TooltipContent>
{t("exportCsvTooltip")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</CardHeader>

View File

@@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-xl font-semibold leading-none tracking-tight",
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}

View File

@@ -0,0 +1,7 @@
export function getSevenDaysAgo() {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to midnight
const sevenDaysAgo = new Date(today);
sevenDaysAgo.setDate(today.getDate() - 7);
return sevenDaysAgo;
}

View File

@@ -181,17 +181,15 @@ export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
export const logQueries = {
requestAnalytics: ({
orgId,
filters,
api
filters
}: {
orgId: string;
filters: LogAnalyticsFilters;
api: AxiosInstance;
}) =>
queryOptions({
queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const,
queryFn: async ({ signal }) => {
const res = await api.get<
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<QueryRequestAnalyticsResponse>
>(`/org/${orgId}/logs/analytics`, {
params: filters,
@@ -252,11 +250,11 @@ export const resourceQueries = {
return res.data.data.targets;
}
}),
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
listNamesPerOrg: (orgId: string) =>
queryOptions({
queryKey: ["RESOURCES_NAMES", orgId] as const,
queryFn: async ({ signal }) => {
const res = await api.get<
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListResourceNamesResponse>
>(`/org/${orgId}/resource-names`, {
signal