diff --git a/README.md b/README.md index 5484f8fb..a842ed3b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@

-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. - | | | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| -| **Manage applications in one place**

Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | | -| **Reverse proxy across networks anywhere**

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. | | -| **Enforce identity and context aware rules**

Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | | -| **Quickly connect Pangolin sites**

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. | | +| **Connect remote networks with sites**

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. | | +| **Browser-based reverse proxy access**

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. | | +| **Client-based private resource access**

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. | | +| **Zero-trust granular access**

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. | | + +## 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 diff --git a/messages/de-DE.json b/messages/de-DE.json index 333c7052..13ab3d11 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -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", diff --git a/messages/en-US.json b/messages/en-US.json index 5ae94f7e..432d853b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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", diff --git a/package-lock.json b/package-lock.json index b594dea0..083b2491 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 63af7f51..78a9ffbc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/screenshots/create-resource.png b/public/screenshots/create-resource.png deleted file mode 100644 index 3b21f22b..00000000 Binary files a/public/screenshots/create-resource.png and /dev/null differ diff --git a/public/screenshots/create-site.png b/public/screenshots/create-site.png index b5ff8048..8d12a962 100644 Binary files a/public/screenshots/create-site.png and b/public/screenshots/create-site.png differ diff --git a/public/screenshots/edit-resource.png b/public/screenshots/edit-resource.png deleted file mode 100644 index 2d21afa6..00000000 Binary files a/public/screenshots/edit-resource.png and /dev/null differ diff --git a/public/screenshots/hero.png b/public/screenshots/hero.png index 86216cf6..f42a830e 100644 Binary files a/public/screenshots/hero.png and b/public/screenshots/hero.png differ diff --git a/public/screenshots/private-resources.png b/public/screenshots/private-resources.png new file mode 100644 index 00000000..f48d9279 Binary files /dev/null and b/public/screenshots/private-resources.png differ diff --git a/public/screenshots/public-resources.png b/public/screenshots/public-resources.png new file mode 100644 index 00000000..f42a830e Binary files /dev/null and b/public/screenshots/public-resources.png differ diff --git a/public/screenshots/resources.png b/public/screenshots/resources.png deleted file mode 100644 index 86216cf6..00000000 Binary files a/public/screenshots/resources.png and /dev/null differ diff --git a/public/screenshots/sites-fade.png b/public/screenshots/sites-fade.png deleted file mode 100644 index 7e21c2cd..00000000 Binary files a/public/screenshots/sites-fade.png and /dev/null differ diff --git a/public/screenshots/sites.png b/public/screenshots/sites.png index 0aaa79d0..86b32b81 100644 Binary files a/public/screenshots/sites.png and b/public/screenshots/sites.png differ diff --git a/public/screenshots/user-devices.png b/public/screenshots/user-devices.png new file mode 100644 index 00000000..7b407cd6 Binary files /dev/null and b/public/screenshots/user-devices.png differ diff --git a/server/lib/consts.ts b/server/lib/consts.ts index b380023e..57149bf6 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -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); diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 549dbffe..e0867dc5 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -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, { diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index 13ba1c95..fda59f39 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -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( diff --git a/server/private/routers/auditLogs/exportAccessAuditLog.ts b/server/private/routers/auditLogs/exportAccessAuditLog.ts index fbeca932..7e912f8c 100644 --- a/server/private/routers/auditLogs/exportAccessAuditLog.ts +++ b/server/private/routers/auditLogs/exportAccessAuditLog.ts @@ -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); diff --git a/server/private/routers/auditLogs/exportActionAuditLog.ts b/server/private/routers/auditLogs/exportActionAuditLog.ts index 1fc4d743..d8987916 100644 --- a/server/private/routers/auditLogs/exportActionAuditLog.ts +++ b/server/private/routers/auditLogs/exportActionAuditLog.ts @@ -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); diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 5d3162aa..eb0cae5d 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -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)), { diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index eca583b4..518eb982 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -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)), { diff --git a/server/routers/auditLogs/exportRequestAuditLog.ts b/server/routers/auditLogs/exportRequestAuditLog.ts index 9e55cfc4..8b70ec5e 100644 --- a/server/routers/auditLogs/exportRequestAuditLog.ts +++ b/server/routers/auditLogs/exportRequestAuditLog.ts @@ -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); diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index 9e4ea17e..a765f176 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -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; 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, diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 663ad787..9cedec63 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -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)), { diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index e0e42754..f1d06566 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -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 diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index e5719e7f..e9ce8e04 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -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; } diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index efc4939b..92704adb 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -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; } diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index 1e7f2476..d5b12ddb 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -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([]); 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(() => { - 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={{ diff --git a/src/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index 68f67b07..344866bb 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -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([]); 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(() => { - 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={{ diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 42cfec57..741dd994 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -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([]); const [isRefreshing, setIsRefreshing] = useState(false); - const [isExporting, setIsExporting] = useState(false); + const [isExporting, startTransition] = useTransition(); // Pagination state const [totalCount, setTotalCount] = useState(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(() => { - 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={{ diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx index 8176273f..9d468e60 100644 --- a/src/components/Credenza.tsx +++ b/src/components/Credenza.tsx @@ -131,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => { const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader; return ( - + {children} ); diff --git a/src/components/LogAnalyticsData.tsx b/src/components/LogAnalyticsData.tsx index 2fd30189..aaf1b344 100644 --- a/src/components/LogAnalyticsData.tsx +++ b/src/components/LogAnalyticsData.tsx @@ -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 }) ); diff --git a/src/components/LogDataTable.tsx b/src/components/LogDataTable.tsx index 492a8c15..e21b5cc0 100644 --- a/src/components/LogDataTable.tsx +++ b/src/components/LogDataTable.tsx @@ -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({ )} {onExport && ( - + + + + + + + {t("exportCsvTooltip")} + + + )} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 3f223d92..328431e9 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef< ; 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 >(`/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 >(`/org/${orgId}/resource-names`, { signal