mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-18 00:29:57 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1cd252ea6 | |||
| 0801c71cf2 | |||
| aea0a2779c | |||
| 89909f3b35 | |||
| e54df0c592 | |||
| 8c579b69e2 | |||
| 1ab28781e3 | |||
| 8c973e5455 | |||
| b1c26b700b | |||
| aada83e57f | |||
| 5fc03bc2e5 | |||
| a75cc9b723 | |||
| 6f1b3266c0 | |||
| 5681ec06cd | |||
| f5b2058112 | |||
| 7f9fe3c67b | |||
| b617670fd8 | |||
| 53d4433d87 | |||
| 36bbb652cc | |||
| 0394289aab | |||
| 52e9a8d43f | |||
| 72cfe81727 | |||
| ed1a7c2723 | |||
| 8a05885192 | |||
| fed9d4b4b5 | |||
| b0d338456f | |||
| 9442ad2214 | |||
| 3fbe8d0618 | |||
| 86a9eadcff | |||
| 6a7be975ed |
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:26.8-alpine
|
||||
FROM node:24.18.1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ Give users a landing page to quickly find and open the resources they can access
|
||||
* Create reusable views for common access patterns
|
||||
|
||||
<img src="public/screenshots/resource-launcher.png" alt="Resource Launcher" width="100%" />
|
||||
<img src="public/screenshots/resource-launcher-expanded.png" alt="Resource Launcher Details Panel" width="100%" />
|
||||
|
||||
## Download Clients
|
||||
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Сигурни ли сте, че искате да рестартирате WireGuard тунела за <b>{name}</b>? Сайтът ще изгуби връзка за кратко.",
|
||||
"siteRestartWarning": "Сайтът ще се изключи за кратко, докато тунелът се рестартира.",
|
||||
"siteRestarted": "Сайтът е рестартиран",
|
||||
"siteRestartedDescription": "WireGuard тунелът е рестартиран.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Неуспешно рестартиране на сайта",
|
||||
"siteErrorRestartDescription": "Възникна грешка при рестартирането на сайта.",
|
||||
"siteSettingDescription": "Конфигурирайте настройките на сайта",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Създайте и управлявайте ресурси, които са достъпни само чрез свързан клиент.",
|
||||
"privateResourcesBannerTitle": "Достъп до частни ресурси с нулево доверие.",
|
||||
"privateResourcesBannerDescription": "Частните ресурси използват сигурност с нулево доверие, осигурявайки че потребителите и машините могат да имат само достъп до ресурси, които вие изрично предоставяте. Свържете потребителските устройства или машинните клиенти, за да получите достъп до тези ресурси чрез сигурна виртуална частна мрежа.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Търсене на ресурси...",
|
||||
"resourceAdd": "Добавете ресурс",
|
||||
"resourceErrorDelte": "Грешка при изтриване на ресурс",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Opravdu chcete restartovat WireGuard tunel pro <b>{name}</b>? Lokalita krátce ztratí konektivitu.",
|
||||
"siteRestartWarning": "Lokalita bude krátce odpojena, zatímco se tunel restartuje.",
|
||||
"siteRestarted": "Lokalita restartována",
|
||||
"siteRestartedDescription": "Tunel WireGuard byl restartován.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Nepodařilo se restartovat lokalitu",
|
||||
"siteErrorRestartDescription": "Při restartování lokality došlo k chybě.",
|
||||
"siteSettingDescription": "Konfigurace nastavení na webu",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Vytváření a správa zdrojů, které jsou přístupné pouze prostřednictvím připojeného klienta",
|
||||
"privateResourcesBannerTitle": "Zero-Trust soukromý přístup",
|
||||
"privateResourcesBannerDescription": "Soukromé prostředky používají zero-trust zabezpečení, což zajišťuje, že uživatelé a zařízení mohou získat přístup pouze k prostředkům, k nimž máte explicitní práva. Připojte zařízení uživatele nebo klientské stroje, abyste získali přístup k těmto prostředkům přes zabezpečenou virtuální soukromou síť.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Prohledat zdroje...",
|
||||
"resourceAdd": "Přidat zdroj",
|
||||
"resourceErrorDelte": "Chyba při odstraňování zdroje",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Er du sikker på, at du vil genstarte WireGuard-tunnelen for <b>{name}</b>? Sitet vil kortvarigt miste forbindelsen.",
|
||||
"siteRestartWarning": "Sitet vil kortvarigt afbryde forbindelse, mens tunnelen genstarter.",
|
||||
"siteRestarted": "Site genstartet",
|
||||
"siteRestartedDescription": "WireGuard-tunnelen er blevet genstartet.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Kunne ikke genstarte site",
|
||||
"siteErrorRestartDescription": "En fejl opstod, mens sitet blev genstartet.",
|
||||
"siteSettingDescription": "Konfigurer indstillingerne for sitet",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Oprette og administrer ressourcer som kun er tilgængelige via en tilsluttet klient",
|
||||
"privateResourcesBannerTitle": "Zero-Trust privat adgang",
|
||||
"privateResourcesBannerDescription": "Private ressourcer bruger Zero-Trust-sikkerhed og sikrer, at brugere og maskiner kun kan få adgang til ressourcer, du eksplicit giver tilladelse til. Tilslut brugerenheder eller maskinklienter for at få adgang til disse ressourcer via et sikkert virtuelt privat netværk.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Søg i ressourcer...",
|
||||
"resourceAdd": "Tilføj ressource",
|
||||
"resourceErrorDelte": "Fejl ved sletning af ressource",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Sind Sie sicher, dass Sie den WireGuard-Tunnel für <b>{name}</b> neu starten möchten? Der Standort wird kurzzeitig die Konnektivität verlieren.",
|
||||
"siteRestartWarning": "Der Standort wird kurzzeitig getrennt, während der Tunnel neu gestartet wird.",
|
||||
"siteRestarted": "Standort neu gestartet",
|
||||
"siteRestartedDescription": "Der WireGuard-Tunnel wurde neu gestartet.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Fehler beim Neustart des Standorts",
|
||||
"siteErrorRestartDescription": "Ein Fehler ist aufgetreten, während der Standort neu gestartet wurde.",
|
||||
"siteSettingDescription": "Standorteinstellungen konfigurieren",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Erstelle und verwalte Ressourcen, die nur über einen verbundenen Client zugänglich sind",
|
||||
"privateResourcesBannerTitle": "Zero-Trust-Zugriff auf private Ressourcen",
|
||||
"privateResourcesBannerDescription": "Private Ressourcen nutzen Zero-Trust und stellen sicher, dass Benutzer und Maschinen nur auf Ressourcen zugreifen können, die Sie explizit gewähren. Verbinden Sie Benutzergeräte oder Maschinen-Clients, um auf diese Ressourcen über ein sicheres virtuelles privates Netzwerk zuzugreifen.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Suche Ressourcen...",
|
||||
"resourceAdd": "Ressource hinzufügen",
|
||||
"resourceErrorDelte": "Fehler beim Löschen der Ressource",
|
||||
|
||||
@@ -234,9 +234,6 @@
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
"privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Search resources...",
|
||||
"resourceAdd": "Add Resource",
|
||||
"resourceErrorDelte": "Error deleting resource",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "¿Estás seguro de que deseas reiniciar el túnel WireGuard para <b>{name}</b>? El sitio perderá conectividad brevemente.",
|
||||
"siteRestartWarning": "El sitio se desconectará brevemente mientras se reinicia el túnel.",
|
||||
"siteRestarted": "Sitio reiniciado",
|
||||
"siteRestartedDescription": "El túnel WireGuard ha sido reiniciado.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Error al reiniciar el sitio",
|
||||
"siteErrorRestartDescription": "Se ha producido un error al reiniciar el sitio.",
|
||||
"siteSettingDescription": "Configurar los ajustes en el sitio",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Crear y administrar recursos que sólo son accesibles a través de un cliente conectado",
|
||||
"privateResourcesBannerTitle": "Acceso privado de confianza cero",
|
||||
"privateResourcesBannerDescription": "Los recursos privados usan seguridad de confianza cero, asegurando que usuarios y máquinas solo puedan acceder a los recursos que usted conceda explícitamente. Conecte dispositivos de usuario o clientes de máquinas para acceder a estos recursos a través de una red privada virtual segura.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Buscar recursos...",
|
||||
"resourceAdd": "Añadir Recurso",
|
||||
"resourceErrorDelte": "Error al eliminar el recurso",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Êtes-vous sûr de vouloir redémarrer le tunnel WireGuard pour <b>{name}</b>? Le site perdra brièvement sa connectivité.",
|
||||
"siteRestartWarning": "Le site sera brièvement déconnecté pendant le redémarrage du tunnel.",
|
||||
"siteRestarted": "Site redémarré",
|
||||
"siteRestartedDescription": "Le tunnel WireGuard a été redémarré.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Échec du redémarrage du site",
|
||||
"siteErrorRestartDescription": "Une erreur s'est produite lors du redémarrage du site.",
|
||||
"siteSettingDescription": "Configurer les paramètres du site",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Créer et gérer des ressources qui ne sont accessibles que via un client connecté",
|
||||
"privateResourcesBannerTitle": "Accès privé sans confiance",
|
||||
"privateResourcesBannerDescription": "Les ressources privées utilisent la sécurité sans confiance, garantissant que les utilisateurs et les machines ne peuvent accéder qu'aux ressources que vous accordez explicitement. Connectez les appareils utilisateur ou les clients machines à ces ressources via un réseau privé virtuel sécurisé.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Chercher des ressources...",
|
||||
"resourceAdd": "Ajouter une ressource",
|
||||
"resourceErrorDelte": "Erreur lors de la de suppression de la ressource",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Sei sicuro di voler riavviare il tunnel WireGuard per <b>{name}</b>? Il sito perderà brevemente la connettività.",
|
||||
"siteRestartWarning": "Il sito si disconnette brevemente mentre il tunnel si riavvia.",
|
||||
"siteRestarted": "Sito riavviato",
|
||||
"siteRestartedDescription": "Il tunnel WireGuard è stato riavviato.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Impossibile riavviare il sito",
|
||||
"siteErrorRestartDescription": "Si è verificato un errore durante il riavvio del sito.",
|
||||
"siteSettingDescription": "Configura le impostazioni del sito",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||
"privateResourcesBannerDescription": "Le risorse private utilizzano la sicurezza zero-trust, assicurandosi che gli utenti e le macchine possano accedere solo alle risorse a cui hai concesso esplicitamente l'accesso. Collega i dispositivi utente o i client macchina per accedere a queste risorse tramite una rete privata virtuale sicura.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Cerca risorse...",
|
||||
"resourceAdd": "Aggiungi Risorsa",
|
||||
"resourceErrorDelte": "Errore nell'eliminare la risorsa",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "<b>{name}</b>의 WireGuard 터널을 재시작하시겠습니까? 이 작업으로 인해 사이트의 연결이 일시적으로 중단될 수 있습니다.",
|
||||
"siteRestartWarning": "터널을 재시작하는 동안 사이트가 일시적으로 연결이 끊깁니다.",
|
||||
"siteRestarted": "사이트가 재시작되었습니다",
|
||||
"siteRestartedDescription": "WireGuard 터널이 재시작되었습니다.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "사이트 재시작 실패",
|
||||
"siteErrorRestartDescription": "사이트를 재시작하는 중 오류가 발생했습니다.",
|
||||
"siteSettingDescription": "사이트에서 설정을 구성하세요.",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "연결된 클라이언트를 통해서만 접근할 수 있는 리소스를 생성하고 관리하세요.",
|
||||
"privateResourcesBannerTitle": "제로 트러스트 개인 접근",
|
||||
"privateResourcesBannerDescription": "개인 리소스는 제로 트러스트 보안을 사용하여, 사용자와 장치가 명시적으로 허용된 리소스에만 접근할 수 있도록 보장합니다. 이러한 리소스에 접근하려면 안전한 가상 사설 네트워크를 통해 사용자 장치 또는 머신 클라이언트를 연결하십시오.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "리소스 검색...",
|
||||
"resourceAdd": "리소스 추가",
|
||||
"resourceErrorDelte": "리소스 삭제 중 오류 발생",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Er du sikker på at du vil starte WireGuard-tunnelen for <b>{name}</b> på nytt? Området vil midlertidig miste tilkoblingen.",
|
||||
"siteRestartWarning": "Området vil kobles kort fra mens tunnelen starter om.",
|
||||
"siteRestarted": "Område startet på nytt",
|
||||
"siteRestartedDescription": "WireGuard-tunnelen er startet på nytt.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Kan ikke starte område på nytt",
|
||||
"siteErrorRestartDescription": "En feil oppstod ved omstart av området.",
|
||||
"siteSettingDescription": "Konfigurere innstillingene på nettstedet",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Opprette og administrere ressurser som bare er tilgjengelige via en tilkoblet klient",
|
||||
"privateResourcesBannerTitle": "Zero-Trust privat tilgang",
|
||||
"privateResourcesBannerDescription": "Private ressurser bruker Zero-Trust-sikkerhet, og sikrer at brukere og maskiner kun kan få tilgang til ressurser du eksplisitt gir tillatelse til. Koble bruker-enheter eller maskinklienter for å få tilgang til disse ressursene via et sikkert virtuelt privat nettverk.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Søk i ressurser...",
|
||||
"resourceAdd": "Legg til ressurs",
|
||||
"resourceErrorDelte": "Feil ved sletting av ressurs",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Weet u zeker dat u de WireGuard-tunnel voor <b>{name}</b> wilt herstarten? De site zal tijdelijk geen connectiviteit hebben.",
|
||||
"siteRestartWarning": "De site zal kort worden losgekoppeld terwijl de tunnel opnieuw wordt gestart.",
|
||||
"siteRestarted": "Site herstart",
|
||||
"siteRestartedDescription": "De WireGuard-tunnel is opnieuw gestart.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Site herstarten mislukt",
|
||||
"siteErrorRestartDescription": "Er is een fout opgetreden tijdens het herstarten van de site.",
|
||||
"siteSettingDescription": "Configureer de instellingen van de site",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Creëer en beheer bronnen die alleen toegankelijk zijn via een verbonden client",
|
||||
"privateResourcesBannerTitle": "Privébronnen via Zero Trust",
|
||||
"privateResourcesBannerDescription": "Privébronnen maken gebruik van zero trust beveiliging, wat ervoor zorgt dat gebruikers en machines alleen toegang kunnen krijgen tot resources die je specifiek toestaat. Verbind gebruikersapparaten of machines om deze middelen te benaderen via een veilig, virtueel privénetwerk.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Zoek bronnen...",
|
||||
"resourceAdd": "Bron toevoegen",
|
||||
"resourceErrorDelte": "Fout bij verwijderen document",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Czy na pewno chcesz uruchomić ponownie tunel WireGuard dla <b>{name}</b>? Strona tymczasowo straci łączność.",
|
||||
"siteRestartWarning": "Strona tymczasowo rozłączy się podczas ponownego uruchamiania tunelu.",
|
||||
"siteRestarted": "Strona zrestartowana",
|
||||
"siteRestartedDescription": "Tunel WireGuard został ponownie uruchomiony.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Nie udało się zrestartować strony",
|
||||
"siteErrorRestartDescription": "Wystąpił błąd podczas ponownego uruchamiania strony.",
|
||||
"siteSettingDescription": "Skonfiguruj ustawienia na stronie",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Twórz i zarządzaj zasobami, które są dostępne tylko za pośrednictwem połączonego klienta",
|
||||
"privateResourcesBannerTitle": "Zero zaufania do prywatnego dostępu",
|
||||
"privateResourcesBannerDescription": "Zasoby prywatne korzystają z zabezpieczeń zero-trust, zapewniając dostęp do zasobów użytkownikom i maszynom, którym wyraźnie udzielasz dostępu. Połącz urządzenia użytkowników lub klientów maszyn z tymi zasobami przez bezpieczną prywatną sieć wirtualną.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Szukaj zasobów...",
|
||||
"resourceAdd": "Dodaj zasób",
|
||||
"resourceErrorDelte": "Błąd podczas usuwania zasobu",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Tem certeza de que deseja reiniciar o túnel WireGuard para <b>{name}</b>? O site perderá brevemente a conectividade.",
|
||||
"siteRestartWarning": "O site será desconectado brevemente enquanto o túnel reinicia.",
|
||||
"siteRestarted": "Site reiniciado",
|
||||
"siteRestartedDescription": "O túnel WireGuard foi reiniciado.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Falha ao reiniciar o site",
|
||||
"siteErrorRestartDescription": "Ocorreu um erro ao reiniciar o site.",
|
||||
"siteSettingDescription": "Configurar as configurações no site",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Criar e gerenciar recursos que só são acessíveis por meio de um cliente conectado",
|
||||
"privateResourcesBannerTitle": "Acesso Privado com Confiança Zero",
|
||||
"privateResourcesBannerDescription": "Os recursos privados usam segurança de zero confiança, garantindo que usuários e máquinas possam acessar apenas os recursos que você concede explicitamente. Conecte dispositivos de usuários ou clientes de máquinas para acessar esses recursos por meio de uma rede privada virtual segura.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Procurar recursos...",
|
||||
"resourceAdd": "Adicionar Recurso",
|
||||
"resourceErrorDelte": "Erro ao apagar recurso",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "Вы уверены, что хотите перезапустить туннель WireGuard для <b>{name}</b>? Сайт кратковременно потеряет соединение.",
|
||||
"siteRestartWarning": "Сайт кратковременно отключится во время перезапуска туннеля.",
|
||||
"siteRestarted": "Сайт перезапущен",
|
||||
"siteRestartedDescription": "Туннель WireGuard был перезапущен.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Не удалось перезапустить сайт",
|
||||
"siteErrorRestartDescription": "Произошла ошибка во время перезапуска сайта.",
|
||||
"siteSettingDescription": "Настройка параметров на сайте",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
|
||||
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
|
||||
"privateResourcesBannerDescription": "Частные ресурсы используют безопасность с нулевым доверием, обеспечивая доступ пользователей и устройств только к ресурсам, к которым вы явно предоставили доступ. Подключите пользовательские устройства или машинных клиентов, чтобы получить доступ к этим ресурсам через безопасную виртуальную частную сеть.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Поиск ресурсов...",
|
||||
"resourceAdd": "Добавить ресурс",
|
||||
"resourceErrorDelte": "Ошибка при удалении ресурса",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "<b>{name}</b> için WireGuard tünelini yeniden başlatmak istediğinizden emin misiniz? Site kısa süreliğine bağlantıyı kaybedecektir.",
|
||||
"siteRestartWarning": "Tünel yeniden başlatılırken site kısa süreliğine kesintiye uğrar.",
|
||||
"siteRestarted": "Site yeniden başlatıldı",
|
||||
"siteRestartedDescription": "WireGuard tüneli yeniden başlatıldı.",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "Sitenin yeniden başlatılması başarısız oldu",
|
||||
"siteErrorRestartDescription": "Site yeniden başlatılırken bir hata oluştu.",
|
||||
"siteSettingDescription": "Sitenizdeki ayarları yapılandırın",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "Sadece bağlı bir istemci aracılığıyla erişilebilen kaynakları oluşturun ve yönetin",
|
||||
"privateResourcesBannerTitle": "Sıfır Güven Özel Erişim",
|
||||
"privateResourcesBannerDescription": "Özel kaynaklar sıfır güven güvenliği kullanır, kullanıcılar ve makinelerin yalnızca açıkça izin verdiğiniz kaynaklara erişmesini sağlar. Bu kaynaklara güvenli bir sanal özel ağ üzerinden erişmek için kullanıcı cihazlarını veya makine müşterilerini bağlayın.",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "Kaynakları ara...",
|
||||
"resourceAdd": "Kaynak Ekle",
|
||||
"resourceErrorDelte": "Kaynak silinirken hata",
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@
|
||||
"siteRestartDialogMessage": "确定要重启<b>{name}</b>的WireGuard隧道吗?站点将暂时断开连接。",
|
||||
"siteRestartWarning": "隧道重启时,站点将暂时断开连接。",
|
||||
"siteRestarted": "站点已重启",
|
||||
"siteRestartedDescription": "WireGuard隧道已重启。",
|
||||
"siteRestartedDescription": "The site has been restarted.",
|
||||
"siteErrorRestart": "重启站点失败",
|
||||
"siteErrorRestartDescription": "重启站点时发生错误。",
|
||||
"siteSettingDescription": "配置站点设置",
|
||||
@@ -234,6 +234,9 @@
|
||||
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
|
||||
"privateResourcesBannerTitle": "零信任私有访问",
|
||||
"privateResourcesBannerDescription": "私有资源采用零信任安全机制,确保只有获得明确授权的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
|
||||
"licenseBillingBannerTitle": "Manage License Billing",
|
||||
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
|
||||
"licenseBillingBannerButton": "Go to Billing",
|
||||
"resourcesSearch": "搜索资源...",
|
||||
"resourceAdd": "添加资源",
|
||||
"resourceErrorDelte": "删除资源时出错",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 711 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 620 KiB |
@@ -93,8 +93,9 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||
</EmailSection>
|
||||
|
||||
<EmailText>
|
||||
For any questions or concerns regarding your license
|
||||
or billing, please reach out to support at{" "}
|
||||
If you need to purchase additional license keys or
|
||||
modify your existing license, please reach out to
|
||||
our support team at{" "}
|
||||
<a
|
||||
href="mailto:support@pangolin.net"
|
||||
className="text-primary font-medium"
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { formatBackupTimestamp } from "./backupFileName";
|
||||
import { assertEquals } from "@test/assert";
|
||||
|
||||
// Local-time constructors are used throughout, matching formatBackupTimestamp,
|
||||
// so these cases do not depend on the machine's timezone.
|
||||
|
||||
function testMonthIsOneIndexed() {
|
||||
console.log("Running month indexing tests...");
|
||||
|
||||
// The case from the report: a backup taken on 12 September 2026 was named
|
||||
// db_2026-8-12_... because Date#getMonth is zero-indexed.
|
||||
{
|
||||
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 35, 56));
|
||||
assertEquals(
|
||||
result,
|
||||
"2026-09-12_20-35-56",
|
||||
"September must render as 09, not 8"
|
||||
);
|
||||
}
|
||||
|
||||
// The other reported name, db_2026-0-23_..., was a January backup.
|
||||
{
|
||||
const result = formatBackupTimestamp(new Date(2026, 0, 23, 20, 25, 49));
|
||||
assertEquals(
|
||||
result,
|
||||
"2026-01-23_20-25-49",
|
||||
"January must render as 01, not 0"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const result = formatBackupTimestamp(new Date(2026, 11, 31, 23, 59, 59));
|
||||
assertEquals(
|
||||
result,
|
||||
"2026-12-31_23-59-59",
|
||||
"December must render as 12"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function testEveryFieldIsZeroPadded() {
|
||||
console.log("Running zero padding tests...");
|
||||
|
||||
// db_2026-8-12_20-36-2 in the report: a single-digit second was not padded.
|
||||
{
|
||||
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 36, 2));
|
||||
assertEquals(
|
||||
result,
|
||||
"2026-09-12_20-36-02",
|
||||
"Single-digit seconds must be padded"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const result = formatBackupTimestamp(new Date(2026, 0, 1, 0, 0, 0));
|
||||
assertEquals(
|
||||
result,
|
||||
"2026-01-01_00-00-00",
|
||||
"Midnight on the first of the month must pad every field"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function testNamesSortChronologically() {
|
||||
console.log("Running sort order tests...");
|
||||
|
||||
// Zero padding means a plain lexicographic sort of the backups directory
|
||||
// lists the backups in the order they were taken.
|
||||
const taken = [
|
||||
new Date(2026, 8, 12, 20, 36, 2),
|
||||
new Date(2026, 0, 23, 20, 25, 49),
|
||||
new Date(2026, 8, 12, 20, 35, 56),
|
||||
new Date(2026, 11, 31, 23, 59, 59)
|
||||
];
|
||||
|
||||
const sorted = taken
|
||||
.map((date) => formatBackupTimestamp(date))
|
||||
.sort();
|
||||
|
||||
assertEquals(
|
||||
sorted.join(","),
|
||||
[
|
||||
"2026-01-23_20-25-49",
|
||||
"2026-09-12_20-35-56",
|
||||
"2026-09-12_20-36-02",
|
||||
"2026-12-31_23-59-59"
|
||||
].join(","),
|
||||
"Backup names must sort into the order the backups were taken"
|
||||
);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
try {
|
||||
testMonthIsOneIndexed();
|
||||
testEveryFieldIsZeroPadded();
|
||||
testNamesSortChronologically();
|
||||
console.log("All tests passed successfully!");
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Builds the timestamp segment of a database backup file name.
|
||||
*
|
||||
* `Date#getMonth` is zero-indexed, so building this inline produced names like
|
||||
* `db_2026-8-12_...` for a backup taken on 12 September 2026. Every field is
|
||||
* also zero-padded, which keeps the names unambiguous and makes them sort
|
||||
* lexicographically in the order they were taken.
|
||||
*
|
||||
* @param date The moment the backup is being taken. Defaults to now.
|
||||
* @returns A timestamp of the form `YYYY-MM-DD_HH-MM-SS`.
|
||||
*/
|
||||
export function formatBackupTimestamp(date: Date = new Date()): string {
|
||||
const pad = (value: number): string => String(value).padStart(2, "0");
|
||||
|
||||
const datePart = [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate())
|
||||
].join("-");
|
||||
|
||||
const timePart = [
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds())
|
||||
].join("-");
|
||||
|
||||
return `${datePart}_${timePart}`;
|
||||
}
|
||||
@@ -243,14 +243,6 @@ export async function handleSubscriptionCreated(
|
||||
`License type determined: ${numUsers} users, ${numSites} sites for subscription ${subscription.id}`
|
||||
);
|
||||
|
||||
// Grace period of 5 days added on top of the current billing
|
||||
// period end (usually ~1 year out) before the license expires
|
||||
const currentPeriodEnd =
|
||||
fullSubscription.items.data[0]?.current_period_end;
|
||||
const expiresAt =
|
||||
(currentPeriodEnd ?? subscription.created) +
|
||||
5 * 24 * 60 * 60;
|
||||
|
||||
const response = await fetch(
|
||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`,
|
||||
{
|
||||
@@ -266,8 +258,7 @@ export async function handleSubscriptionCreated(
|
||||
paidFor: true,
|
||||
users: numUsers,
|
||||
sites: numSites,
|
||||
tier: tier,
|
||||
expiresAt: expiresAt
|
||||
tier: tier
|
||||
})
|
||||
}
|
||||
);
|
||||
@@ -276,13 +267,6 @@ export async function handleSubscriptionCreated(
|
||||
|
||||
logger.debug(`Fossorial API response: ${JSON.stringify(data)}`);
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
logger.error(
|
||||
`Fossorial API returned ${response.status} when setting paid-for for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${JSON.stringify(data)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (customer.email) {
|
||||
logger.debug(
|
||||
`Sending license key email to ${customer.email} for subscription ${subscription.id}`
|
||||
|
||||
@@ -125,7 +125,9 @@ export async function handleSubscriptionDeleted(
|
||||
`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
|
||||
);
|
||||
try {
|
||||
const invalidateResponse = await fetch(
|
||||
// WARNING:
|
||||
// this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId
|
||||
await fetch(
|
||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -137,18 +139,9 @@ export async function handleSubscriptionDeleted(
|
||||
},
|
||||
body: JSON.stringify({
|
||||
orgId: customer.orgId,
|
||||
licenseKeyId: parseInt(
|
||||
subscription.metadata.licenseKeyId
|
||||
)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!invalidateResponse.ok) {
|
||||
logger.error(
|
||||
`Fossorial API returned ${invalidateResponse.status} when invalidating license for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${await invalidateResponse.text()}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`,
|
||||
|
||||
@@ -23,10 +23,7 @@ import {
|
||||
} from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import {
|
||||
getFeatureIdByMetricId,
|
||||
getFeatureIdByPriceId
|
||||
} from "@server/lib/billing/features";
|
||||
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { getSubType, SubscriptionType } from "./getSubType";
|
||||
@@ -69,8 +66,7 @@ export async function handleSubscriptionUpdated(
|
||||
.limit(1);
|
||||
|
||||
const type = getSubType(fullSubscription);
|
||||
const previousType =
|
||||
existingSubscription.type as SubscriptionType | null;
|
||||
const previousType = existingSubscription.type as SubscriptionType | null;
|
||||
|
||||
// If the subscription has been manually overridden, we lock the
|
||||
// status down so Stripe webhooks can no longer change it.
|
||||
@@ -104,11 +100,7 @@ export async function handleSubscriptionUpdated(
|
||||
logger.info(
|
||||
`Tier change detected for org ${customer.orgId}: ${previousType} -> ${type}`
|
||||
);
|
||||
await handleTierChange(
|
||||
customer.orgId,
|
||||
type,
|
||||
previousType ?? undefined
|
||||
);
|
||||
await handleTierChange(customer.orgId, type, previousType ?? undefined);
|
||||
}
|
||||
|
||||
// Upsert subscription items
|
||||
@@ -121,8 +113,7 @@ export async function handleSubscriptionUpdated(
|
||||
|
||||
const itemsToUpsert = fullSubscription.items.data.map((item) => {
|
||||
// Try to get featureId from price
|
||||
let featureId: string | null =
|
||||
getFeatureIdByPriceId(item.price.id) || null;
|
||||
let featureId: string | null = getFeatureIdByPriceId(item.price.id) || null;
|
||||
|
||||
// If no match, try to preserve existing featureId
|
||||
if (!featureId) {
|
||||
@@ -311,20 +302,14 @@ export async function handleSubscriptionUpdated(
|
||||
logger.info(
|
||||
`Subscription ${subscription.id} for org ${customer.orgId} is ${effectiveStatus}, disabling paid features`
|
||||
);
|
||||
await handleTierChange(
|
||||
customer.orgId,
|
||||
null,
|
||||
previousType ?? undefined
|
||||
);
|
||||
await handleTierChange(customer.orgId, null, previousType ?? undefined);
|
||||
}
|
||||
} else if (type === "license") {
|
||||
if (
|
||||
effectiveStatus === "canceled" ||
|
||||
effectiveStatus == "unpaid" ||
|
||||
effectiveStatus == "incomplete_expired"
|
||||
) {
|
||||
if (effectiveStatus === "canceled" || effectiveStatus == "unpaid" || effectiveStatus == "incomplete_expired") {
|
||||
try {
|
||||
const invalidateResponse = await fetch(
|
||||
// WARNING:
|
||||
// this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId
|
||||
await fetch(
|
||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -335,95 +320,16 @@ export async function handleSubscriptionUpdated(
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
orgId: customer.orgId,
|
||||
licenseKeyId: parseInt(
|
||||
subscription.metadata.licenseKeyId
|
||||
)
|
||||
orgId: customer.orgId
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!invalidateResponse.ok) {
|
||||
logger.error(
|
||||
`Fossorial API returned ${invalidateResponse.status} when invalidating license for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${await invalidateResponse.text()}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
} else if (effectiveStatus === "active" && previousAttributes) {
|
||||
// Detect a successful renewal: the billing period rolled
|
||||
// forward (the invoice was paid and the new period began
|
||||
// right where the previous one ended).
|
||||
const currentItem = fullSubscription.items.data[0];
|
||||
const prevItems = previousAttributes.items?.data;
|
||||
const prevItem = Array.isArray(prevItems)
|
||||
? prevItems.find(
|
||||
(pi: any) => pi.id === currentItem?.id
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const renewed =
|
||||
currentItem &&
|
||||
prevItem?.current_period_end &&
|
||||
currentItem.current_period_start ===
|
||||
prevItem.current_period_end &&
|
||||
currentItem.current_period_start >
|
||||
prevItem.current_period_start;
|
||||
|
||||
if (renewed) {
|
||||
const licenseKeyId =
|
||||
subscription.metadata.licenseKeyId;
|
||||
|
||||
if (!licenseKeyId) {
|
||||
logger.error(
|
||||
`No licenseKeyId in metadata for subscription ${subscription.id}, cannot extend license.`
|
||||
);
|
||||
} else {
|
||||
// Grace period of 5 days added on top of the new
|
||||
// billing period end (usually ~1 year out)
|
||||
const expiresAt =
|
||||
currentItem.current_period_end +
|
||||
5 * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const extendResponse = await fetch(
|
||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/extend`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"api-key":
|
||||
privateConfig.getRawPrivateConfig()
|
||||
.server.fossorial_api_key!,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
licenseId: parseInt(licenseKeyId),
|
||||
expiresAt: expiresAt
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!extendResponse.ok) {
|
||||
logger.error(
|
||||
`Fossorial API returned ${extendResponse.status} when extending license ${licenseKeyId} for subscription ${subscription.id}: ${await extendResponse.text()}`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Extended license ${licenseKeyId} for subscription ${subscription.id} to expire at ${expiresAt}.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error notifying Fossorial API of license renewal for subscription ${subscription.id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,6 @@ export async function generateNewEnterpriseLicense(
|
||||
);
|
||||
}
|
||||
|
||||
const licenseKeyValue = apiResponse?.data?.licenseKey?.licenseKey;
|
||||
|
||||
// check if we already have a customer for this org
|
||||
const [customer] = await db
|
||||
.select()
|
||||
@@ -131,16 +129,6 @@ export async function generateNewEnterpriseLicense(
|
||||
], // Start with the standard feature set that matches the free limits
|
||||
customer: customer.customerId,
|
||||
mode: "subscription",
|
||||
subscription_data: {
|
||||
description: licenseKeyValue
|
||||
? `License ${licenseKeyValue}`
|
||||
: `License key ID ${keyId}`,
|
||||
metadata: {
|
||||
licenseKeyId: keyId.toString(),
|
||||
licenseKey: licenseKeyValue ?? "",
|
||||
tier: licenseData.tier
|
||||
}
|
||||
},
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true`
|
||||
|
||||
@@ -5,7 +5,6 @@ import path from "path";
|
||||
import semver from "semver";
|
||||
import { versionMigrations } from "../db/sqlite";
|
||||
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
||||
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||
import { SqliteError } from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import { build } from "@server/build";
|
||||
@@ -122,7 +121,7 @@ function backupDb() {
|
||||
// copy the db.sqlite file to backups
|
||||
// add the date to the filename
|
||||
const date = new Date();
|
||||
const dateString = formatBackupTimestamp(date);
|
||||
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
||||
const dbPath = path.join(dbDir, "db.sqlite");
|
||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||
fs.copyFileSync(dbPath, backupPath);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { sql } from "drizzle-orm";
|
||||
|
||||
const version = "1.23.0";
|
||||
|
||||
await migration();
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
@@ -10,11 +12,11 @@ export default async function migration() {
|
||||
await db.execute(sql`BEGIN`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agent" varchar;
|
||||
ALTER TABLE "newt" ADD COLUMN "agent" varchar;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agentVersion" varchar;
|
||||
ALTER TABLE "newt" ADD COLUMN "agentVersion" varchar;
|
||||
`);
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
users
|
||||
} from "../../db/sqlite";
|
||||
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
@@ -35,7 +34,7 @@ export default async function migration() {
|
||||
// copy the db.sqlite file to backups
|
||||
// add the date to the filename
|
||||
const date = new Date();
|
||||
const dateString = formatBackupTimestamp(date);
|
||||
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
||||
const dbPath = path.join(dbDir, "db.sqlite");
|
||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||
fs.copyFileSync(dbPath, backupPath);
|
||||
|
||||
@@ -16,13 +16,13 @@ export default async function migration() {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
`
|
||||
ALTER TABLE 'newt' ADD COLUMN 'agent' text;
|
||||
ALTER TABLE 'newt' ADD 'agent' text;
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
ALTER TABLE 'newt' ADD COLUMN 'agentVersion' text;
|
||||
ALTER TABLE 'newt' ADD 'agentVersion' text;
|
||||
`
|
||||
).run();
|
||||
})();
|
||||
|
||||
@@ -749,6 +749,12 @@ export default function BillingPage() {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Get license key count
|
||||
const getLicenseKeyCount = (): number => {
|
||||
if (!licenseSubscription?.items) return 0;
|
||||
return licenseSubscription.items.length;
|
||||
};
|
||||
|
||||
// Check if downgrading to a tier would violate current usage limits
|
||||
const checkLimitViolations = (
|
||||
targetTier: Tier | "basic"
|
||||
@@ -1539,7 +1545,7 @@ export default function BillingPage() {
|
||||
</SettingsSection>
|
||||
|
||||
{/* Paid License Keys Section */}
|
||||
{licenseSubscription && (
|
||||
{(licenseSubscription || getLicenseKeyCount() > 0) && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
@@ -1555,6 +1561,22 @@ export default function BillingPage() {
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">
|
||||
{t("billingCurrentKeys") ||
|
||||
"Current Keys"}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-semibold">
|
||||
{getLicenseKeyCount()}
|
||||
</span>
|
||||
<span className="text-lg">
|
||||
{getLicenseKeyCount() === 1
|
||||
? "key"
|
||||
: "keys"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleModifySubscription}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import GenerateLicenseKeysTable from "@app/components/GenerateLicenseKeysTable";
|
||||
import LicenseBillingBanner from "@app/components/LicenseBillingBanner";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types";
|
||||
@@ -27,16 +26,5 @@ export default async function Page({ params }: Props) {
|
||||
licenseKeys = data.data.data;
|
||||
} catch {}
|
||||
|
||||
const hasNonPersonalLicenseKey = licenseKeys.some(
|
||||
(key) => key.tier !== "personal"
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasNonPersonalLicenseKey && (
|
||||
<LicenseBillingBanner orgId={orgId} />
|
||||
)}
|
||||
<GenerateLicenseKeysTable licenseKeys={licenseKeys} orgId={orgId} />
|
||||
</>
|
||||
);
|
||||
return <GenerateLicenseKeysTable licenseKeys={licenseKeys} orgId={orgId} />;
|
||||
}
|
||||
|
||||
@@ -76,13 +76,11 @@ export default function PrivateResourceInferencePage() {
|
||||
})
|
||||
),
|
||||
httpConfigSubdomain: z.string().nullish(),
|
||||
httpConfigDomainId: z
|
||||
.string()
|
||||
.min(1, { message: t("domainRequired") }),
|
||||
httpConfigDomainId: z.string().nullish(),
|
||||
httpConfigFullDomain: z.string().nullish(),
|
||||
ssl: z.boolean().optional()
|
||||
}),
|
||||
[t]
|
||||
[]
|
||||
);
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -105,7 +103,7 @@ export default function PrivateResourceInferencePage() {
|
||||
defaultValues: {
|
||||
providers: [],
|
||||
httpConfigSubdomain: siteResource.subdomain ?? null,
|
||||
httpConfigDomainId: siteResource.domainId ?? "",
|
||||
httpConfigDomainId: siteResource.domainId ?? null,
|
||||
httpConfigFullDomain: siteResource.fullDomain ?? null,
|
||||
ssl: siteResource.ssl ?? false
|
||||
}
|
||||
@@ -291,74 +289,50 @@ export default function PrivateResourceInferencePage() {
|
||||
</SettingsSubsectionHeader>
|
||||
</SettingsFormCell>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="httpConfigDomainId"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<DomainPicker
|
||||
key={`inference-domain-${siteResource.id}`}
|
||||
orgId={
|
||||
siteResource.orgId
|
||||
}
|
||||
cols={2}
|
||||
hideFreeDomain
|
||||
defaultSubdomain={
|
||||
httpConfigSubdomain ??
|
||||
undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
httpConfigDomainId ??
|
||||
undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
httpConfigFullDomain ??
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (res === null) {
|
||||
form.setValue(
|
||||
"httpConfigSubdomain",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigDomainId",
|
||||
"",
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigFullDomain",
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
form.setValue(
|
||||
"httpConfigSubdomain",
|
||||
res.subdomain ??
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigDomainId",
|
||||
res.domainId,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigFullDomain",
|
||||
res.fullDomain
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<DomainPicker
|
||||
key={`inference-domain-${siteResource.id}`}
|
||||
orgId={siteResource.orgId}
|
||||
cols={2}
|
||||
hideFreeDomain
|
||||
defaultSubdomain={
|
||||
httpConfigSubdomain ?? undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
httpConfigDomainId ?? undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
httpConfigFullDomain ??
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(res) => {
|
||||
if (res === null) {
|
||||
form.setValue(
|
||||
"httpConfigSubdomain",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigDomainId",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigFullDomain",
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
form.setValue(
|
||||
"httpConfigSubdomain",
|
||||
res.subdomain ?? null
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigDomainId",
|
||||
res.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"httpConfigFullDomain",
|
||||
res.fullDomain
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
<SettingsFormCell span="half">
|
||||
|
||||
@@ -139,22 +139,6 @@ export default function GeneralForm() {
|
||||
: "Port number should not be set for HTTP resources",
|
||||
path: ["proxyPort"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (
|
||||
["http", "ssh", "rdp", "vnc", "inference"].includes(
|
||||
resource.mode
|
||||
)
|
||||
) {
|
||||
return !!data.domainId;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: t("domainRequired"),
|
||||
path: ["domainId"]
|
||||
}
|
||||
);
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -450,87 +434,63 @@ export default function GeneralForm() {
|
||||
resource.mode
|
||||
) && (
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="domainId"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div id="resource-domain-picker">
|
||||
<DomainPicker
|
||||
allowWildcard={
|
||||
resource.mode !==
|
||||
"inference"
|
||||
}
|
||||
key={
|
||||
resource.resourceId
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
cols={2}
|
||||
defaultSubdomain={
|
||||
form.watch(
|
||||
"subdomain"
|
||||
) ??
|
||||
undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch(
|
||||
"domainId"
|
||||
) ??
|
||||
undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
resourceFullDomainName ||
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (
|
||||
res ===
|
||||
null
|
||||
) {
|
||||
form.setValue(
|
||||
"domainId",
|
||||
undefined,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://`
|
||||
);
|
||||
return;
|
||||
}
|
||||
form.setValue(
|
||||
"domainId",
|
||||
res.domainId,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
res.subdomain ??
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div id="resource-domain-picker">
|
||||
<DomainPicker
|
||||
allowWildcard={
|
||||
resource.mode !==
|
||||
"inference"
|
||||
}
|
||||
key={
|
||||
resource.resourceId
|
||||
}
|
||||
orgId={orgId as string}
|
||||
cols={2}
|
||||
defaultSubdomain={
|
||||
form.watch(
|
||||
"subdomain"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch(
|
||||
"domainId"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
resourceFullDomainName ||
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (res === null) {
|
||||
form.setValue(
|
||||
"domainId",
|
||||
undefined
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://`
|
||||
);
|
||||
return;
|
||||
}
|
||||
form.setValue(
|
||||
"domainId",
|
||||
res.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
res.subdomain ??
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
{!["tcp", "udp", "inference"].includes(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Globe, CreditCard, ArrowRight } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import DismissableBanner from "./DismissableBanner";
|
||||
|
||||
type LicenseBillingBannerProps = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export const LicenseBillingBanner = ({ orgId }: LicenseBillingBannerProps) => {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DismissableBanner
|
||||
storageKey="license-billing-banner-dismissed"
|
||||
version={1}
|
||||
title={t("licenseBillingBannerTitle")}
|
||||
titleIcon={<Globe className="w-5 h-5 text-primary" />}
|
||||
description={t("licenseBillingBannerDescription")}
|
||||
>
|
||||
<Link href={`/${orgId}/settings/billing`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
{t("licenseBillingBannerButton")}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</DismissableBanner>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicenseBillingBanner;
|
||||
@@ -330,7 +330,7 @@ export default function NewPricingLicenseForm({
|
||||
cols={2}
|
||||
/>
|
||||
<a
|
||||
href="https://pangolin.net/pricing#Self-Hosted"
|
||||
href="https://pangolin.net/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
|
||||
@@ -63,12 +63,6 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
) : null;
|
||||
|
||||
if (site.type === "newt") {
|
||||
// agent and agentVersion were added after newtVersion, so a
|
||||
// site still running an older Newt reports only newtVersion.
|
||||
// Without these fallbacks the badge renders with no label and
|
||||
// no version at all.
|
||||
const agentLabel = site.agent == "cli" ? "Pangolin CLI" : "Newt";
|
||||
const agentVersion = site.agentVersion ?? site.newtVersion;
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
@@ -85,12 +79,13 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("agent")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{agentLabel}</span>
|
||||
{agentVersion && (
|
||||
<span>v{agentVersion}</span>
|
||||
)}
|
||||
</div>
|
||||
{site.agent == "newt" ? "Newt" : null}
|
||||
{site.agent == "cli"
|
||||
? "Pangolin CLI"
|
||||
: null}{" "}
|
||||
{site.agentVersion
|
||||
? `v${site.agentVersion}`
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{endpointSection}
|
||||
|
||||
@@ -373,7 +373,7 @@ export default function SitesTable({
|
||||
accessorKey: "type",
|
||||
friendlyName: t("agent"),
|
||||
header: () => {
|
||||
return <span className="p-3">{t("agent")}</span>;
|
||||
return <span className="p-3">{t("type")}</span>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
@@ -386,27 +386,25 @@ export default function SitesTable({
|
||||
);
|
||||
|
||||
if (originalRow.type === "newt") {
|
||||
if (!originalRow.agent && !originalRow.newtVersion) {
|
||||
// it has not checked in yet
|
||||
if (!originalRow.agent) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
// agent and agentVersion were added after newtVersion, so a
|
||||
// site still running an older Newt reports only newtVersion.
|
||||
// Without these fallbacks the badge renders with no label and
|
||||
// no version at all.
|
||||
const agentLabel =
|
||||
originalRow.agent == "cli"
|
||||
? "Pangolin CLI"
|
||||
: "Newt";
|
||||
const agentVersion =
|
||||
originalRow.agentVersion ?? originalRow.newtVersion;
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Badge variant="secondary">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{agentLabel}</span>
|
||||
{agentVersion && (
|
||||
<span>v{agentVersion}</span>
|
||||
<span>
|
||||
{originalRow.agent == "newt"
|
||||
? "Newt"
|
||||
: null}
|
||||
{originalRow.agent == "cli"
|
||||
? "Pangolin CLI"
|
||||
: null}
|
||||
</span>
|
||||
{originalRow.agentVersion && (
|
||||
<span>
|
||||
v{originalRow.agentVersion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
|
||||
@@ -150,7 +150,7 @@ Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
EnvironmentFile=/etc/pangolin/pangolin-site.env
|
||||
ExecStart=/usr/local/bin/pangolin up site
|
||||
ExecStart=/home/owen/fossorial/cli/bin/pangolin up site
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
|
||||
@@ -523,13 +523,6 @@ export function createCreateFormSchema(t: TranslateFn) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.mode === "inference" && !data.httpConfigDomainId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("domainRequired"),
|
||||
path: ["httpConfigDomainId"]
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user