mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-11 04:16:38 +00:00
Compare commits
18 Commits
private-ht
...
crowdin_de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0665ce96a | ||
|
|
80b7f5eda5 | ||
|
|
d341315e08 | ||
|
|
5e4449e5cc | ||
|
|
afbd3a539e | ||
|
|
fb622a83fd | ||
|
|
34626fac24 | ||
|
|
8dc0eb570f | ||
|
|
23b0be42cb | ||
|
|
059575f50e | ||
|
|
ced4bb7df0 | ||
|
|
4c1bc953fa | ||
|
|
9af84c7d02 | ||
|
|
860cfee2f1 | ||
|
|
0391296181 | ||
|
|
4de9afee41 | ||
|
|
660c8fb6f7 | ||
|
|
6c66053ebe |
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Безплатен предоставен домейн",
|
"domainPickerFreeProvidedDomain": "Безплатен предоставен домейн",
|
||||||
"domainPickerVerified": "Проверено",
|
"domainPickerVerified": "Проверено",
|
||||||
"domainPickerUnverified": "Непроверено",
|
"domainPickerUnverified": "Непроверено",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
|
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
|
||||||
"domainPickerError": "Грешка",
|
"domainPickerError": "Грешка",
|
||||||
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
|
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména",
|
"domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména",
|
||||||
"domainPickerVerified": "Ověřeno",
|
"domainPickerVerified": "Ověřeno",
|
||||||
"domainPickerUnverified": "Neověřeno",
|
"domainPickerUnverified": "Neověřeno",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.",
|
"domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.",
|
||||||
"domainPickerError": "Chyba",
|
"domainPickerError": "Chyba",
|
||||||
"domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace",
|
"domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
||||||
"domainPickerVerified": "Verifiziert",
|
"domainPickerVerified": "Verifiziert",
|
||||||
"domainPickerUnverified": "Nicht verifiziert",
|
"domainPickerUnverified": "Nicht verifiziert",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
||||||
"domainPickerError": "Fehler",
|
"domainPickerError": "Fehler",
|
||||||
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains",
|
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains",
|
||||||
|
|||||||
@@ -1817,11 +1817,6 @@
|
|||||||
"editInternalResourceDialogModePort": "Port",
|
"editInternalResourceDialogModePort": "Port",
|
||||||
"editInternalResourceDialogModeHost": "Host",
|
"editInternalResourceDialogModeHost": "Host",
|
||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
|
||||||
"editInternalResourceDialogScheme": "Scheme",
|
|
||||||
"editInternalResourceDialogEnableSsl": "Enable SSL",
|
|
||||||
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
|
||||||
"editInternalResourceDialogDestination": "Destination",
|
"editInternalResourceDialogDestination": "Destination",
|
||||||
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||||
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
@@ -1865,19 +1860,11 @@
|
|||||||
"createInternalResourceDialogModePort": "Port",
|
"createInternalResourceDialogModePort": "Port",
|
||||||
"createInternalResourceDialogModeHost": "Host",
|
"createInternalResourceDialogModeHost": "Host",
|
||||||
"createInternalResourceDialogModeCidr": "CIDR",
|
"createInternalResourceDialogModeCidr": "CIDR",
|
||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
|
||||||
"scheme": "Scheme",
|
|
||||||
"createInternalResourceDialogScheme": "Scheme",
|
|
||||||
"createInternalResourceDialogEnableSsl": "Enable SSL",
|
|
||||||
"createInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
|
||||||
"createInternalResourceDialogDestination": "Destination",
|
"createInternalResourceDialogDestination": "Destination",
|
||||||
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||||
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||||
"createInternalResourceDialogAlias": "Alias",
|
"createInternalResourceDialogAlias": "Alias",
|
||||||
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||||
"internalResourceDownstreamSchemeRequired": "Scheme is required for HTTP resources",
|
|
||||||
"internalResourceHttpPortRequired": "Destination port is required for HTTP resources",
|
|
||||||
"siteConfiguration": "Configuration",
|
"siteConfiguration": "Configuration",
|
||||||
"siteAcceptClientConnections": "Accept Client Connections",
|
"siteAcceptClientConnections": "Accept Client Connections",
|
||||||
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
|
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
|
||||||
@@ -2129,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
"domainPickerVerified": "Verified",
|
"domainPickerVerified": "Verified",
|
||||||
"domainPickerUnverified": "Unverified",
|
"domainPickerUnverified": "Unverified",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||||
"domainPickerError": "Error",
|
"domainPickerError": "Error",
|
||||||
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||||
@@ -2673,12 +2659,8 @@
|
|||||||
"editInternalResourceDialogAddUsers": "Add Users",
|
"editInternalResourceDialogAddUsers": "Add Users",
|
||||||
"editInternalResourceDialogAddClients": "Add Clients",
|
"editInternalResourceDialogAddClients": "Add Clients",
|
||||||
"editInternalResourceDialogDestinationLabel": "Destination",
|
"editInternalResourceDialogDestinationLabel": "Destination",
|
||||||
"editInternalResourceDialogDestinationDescription": "Choose where this resource runs and how clients reach it, then complete the settings that apply to your setup.",
|
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
"editInternalResourceDialogIcmp": "ICMP",
|
"editInternalResourceDialogIcmp": "ICMP",
|
||||||
@@ -2717,8 +2699,6 @@
|
|||||||
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||||
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
||||||
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
|
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
|
||||||
"privateMaintenanceScreenTitle": "Private Placeholder Screen",
|
|
||||||
"privateMaintenanceScreenMessage": "This domain is being used on a private resource. Please connect using the Pangolin client to access this resource.",
|
|
||||||
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
|
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
|
||||||
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
||||||
"editDomain": "Edit Domain",
|
"editDomain": "Edit Domain",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||||
"domainPickerVerified": "Verificado",
|
"domainPickerVerified": "Verificado",
|
||||||
"domainPickerUnverified": "Sin verificar",
|
"domainPickerUnverified": "Sin verificar",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
||||||
"domainPickerError": "Error",
|
"domainPickerError": "Error",
|
||||||
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||||
"domainPickerVerified": "Vérifié",
|
"domainPickerVerified": "Vérifié",
|
||||||
"domainPickerUnverified": "Non vérifié",
|
"domainPickerUnverified": "Non vérifié",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.",
|
"domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.",
|
||||||
"domainPickerError": "Erreur",
|
"domainPickerError": "Erreur",
|
||||||
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Creare l'organizzazione, il sito e le risorse",
|
"setupCreate": "Creare l'organizzazione, il sito e le risorse",
|
||||||
"headerAuthCompatibilityInfo": "Abilita questo per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.",
|
"headerAuthCompatibilityInfo": "Abilita questa funzionalità per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.",
|
||||||
"headerAuthCompatibility": "Compatibilità estesa",
|
"headerAuthCompatibility": "Compatibilità estesa",
|
||||||
"setupNewOrg": "Nuova Organizzazione",
|
"setupNewOrg": "Nuova Organizzazione",
|
||||||
"setupCreateOrg": "Crea Organizzazione",
|
"setupCreateOrg": "Crea Organizzazione",
|
||||||
"setupCreateResources": "Crea Risorse",
|
"setupCreateResources": "Crea Risorse",
|
||||||
"setupOrgName": "Nome Dell'Organizzazione",
|
"setupOrgName": "Nome dell'Organizzazione",
|
||||||
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
||||||
"orgId": "Id Organizzazione",
|
"orgId": "Id Organizzazione",
|
||||||
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
||||||
"setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.",
|
"setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.",
|
||||||
"componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.",
|
"componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.",
|
||||||
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
||||||
"welcome": "Benvenuti a Pangolin",
|
"welcome": "Benvenuto su Pangolin!",
|
||||||
"welcomeTo": "Benvenuto a",
|
"welcomeTo": "Benvenuto su Pangolin!",
|
||||||
"componentsCreateOrg": "Crea un'organizzazione",
|
"componentsCreateOrg": "Crea un'organizzazione",
|
||||||
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
|
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
|
||||||
"componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.",
|
"componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.",
|
"inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.",
|
||||||
"inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.",
|
"inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.",
|
||||||
"inviteCreateUser": "Si prega di creare un account prima.",
|
"inviteCreateUser": "Si prega di creare un account prima.",
|
||||||
"goHome": "Vai A Home",
|
"goHome": "Vai alla Home",
|
||||||
"inviteLogInOtherUser": "Accedi come utente diverso",
|
"inviteLogInOtherUser": "Accedi come utente diverso",
|
||||||
"createAnAccount": "Crea un account",
|
"createAnAccount": "Crea un account",
|
||||||
"inviteNotAccepted": "Invito Non Accettato",
|
"inviteNotAccepted": "Invito Non Accettato",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
||||||
"siteDelete": "Elimina Sito",
|
"siteDelete": "Elimina Sito",
|
||||||
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli obiettivi associati al sito verranno rimossi.",
|
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.",
|
||||||
"siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
|
"siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
|
||||||
"siteManageSites": "Gestisci Siti",
|
"siteManageSites": "Gestisci Siti",
|
||||||
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
||||||
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
||||||
"toggle": "Attiva/disattiva",
|
"toggle": "Attiva/disattiva",
|
||||||
"dockerCompose": "Composizione Docker",
|
"dockerCompose": "Docker Compose",
|
||||||
"dockerRun": "Corsa Docker",
|
"dockerRun": "Corsa Docker",
|
||||||
"siteLearnLocal": "I siti locali non tunnel, saperne di più",
|
"siteLearnLocal": "I siti locali non effettuano il tunnel, per saperne di più",
|
||||||
"siteConfirmCopy": "Ho copiato la configurazione",
|
"siteConfirmCopy": "Ho copiato la configurazione",
|
||||||
"searchSitesProgress": "Cerca siti...",
|
"searchSitesProgress": "Cerca siti...",
|
||||||
"siteAdd": "Aggiungi Sito",
|
"siteAdd": "Aggiungi Sito",
|
||||||
@@ -88,29 +88,29 @@
|
|||||||
"operatingSystem": "Sistema Operativo",
|
"operatingSystem": "Sistema Operativo",
|
||||||
"commands": "Comandi",
|
"commands": "Comandi",
|
||||||
"recommended": "Consigliato",
|
"recommended": "Consigliato",
|
||||||
"siteNewtDescription": "Per la migliore esperienza utente, utilizzare Newt. Utilizza WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.",
|
"siteNewtDescription": "Per la migliore esperienza utente utilizzare Newt, che usa WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.",
|
||||||
"siteRunsInDocker": "Esegue nel Docker",
|
"siteRunsInDocker": "Esegue nel Docker",
|
||||||
"siteRunsInShell": "Esegue in shell su macOS, Linux e Windows",
|
"siteRunsInShell": "Esegue in shell su macOS, Linux e Windows",
|
||||||
"siteErrorDelete": "Errore nell'eliminare il sito",
|
"siteErrorDelete": "Errore nella eliminazione del sito",
|
||||||
"siteErrorUpdate": "Impossibile aggiornare il sito",
|
"siteErrorUpdate": "Impossibile aggiornare il sito",
|
||||||
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
||||||
"siteUpdated": "Sito aggiornato",
|
"siteUpdated": "Sito aggiornato",
|
||||||
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
||||||
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
||||||
"siteSettingDescription": "Configura le impostazioni del sito",
|
"siteSettingDescription": "Configura le impostazioni del sito",
|
||||||
"siteSetting": "Impostazioni {siteName}",
|
"siteSetting": "Impostazioni del sito {siteName}",
|
||||||
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
||||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
||||||
"siteWg": "WireGuard Base",
|
"siteWg": "WireGuard Base",
|
||||||
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
"siteWgDescription": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||||
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
"siteWgDescriptionSaas": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
||||||
"siteSeeAll": "Vedi Tutti I Siti",
|
"siteSeeAll": "Vedi Tutti I Siti",
|
||||||
"siteTunnelDescription": "Determinare come si desidera connettersi al sito",
|
"siteTunnelDescription": "Selezionare la modalità con la quale si desidera connettersi al sito",
|
||||||
"siteNewtCredentials": "Credenziali",
|
"siteNewtCredentials": "Credenziali",
|
||||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server",
|
"siteNewtCredentialsDescription": "Questo è come il sito si autenticherà con il server",
|
||||||
"remoteNodeCredentialsDescription": "Questo è come il nodo remoto si autenticherà con il server",
|
"remoteNodeCredentialsDescription": "Questo è il modo in cui il nodo remoto si autenticherà con il server",
|
||||||
"siteCredentialsSave": "Salva le credenziali",
|
"siteCredentialsSave": "Salva le credenziali",
|
||||||
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
||||||
"siteInfo": "Informazioni Sito",
|
"siteInfo": "Informazioni Sito",
|
||||||
@@ -140,8 +140,8 @@
|
|||||||
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
||||||
"shareTitleOptional": "Titolo (facoltativo)",
|
"shareTitleOptional": "Titolo (facoltativo)",
|
||||||
"expireIn": "Scadenza In",
|
"expireIn": "Scadenza In",
|
||||||
"neverExpire": "Mai scadere",
|
"neverExpire": "Nessuna scadenza",
|
||||||
"shareExpireDescription": "Il tempo di scadenza è per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
|
"shareExpireDescription": "Il tempo di scadenza indica per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
|
||||||
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
|
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
|
||||||
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
||||||
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
||||||
@@ -161,9 +161,9 @@
|
|||||||
"never": "Mai",
|
"never": "Mai",
|
||||||
"shareErrorSelectResource": "Seleziona una risorsa",
|
"shareErrorSelectResource": "Seleziona una risorsa",
|
||||||
"proxyResourceTitle": "Gestisci Risorse Pubbliche",
|
"proxyResourceTitle": "Gestisci Risorse Pubbliche",
|
||||||
"proxyResourceDescription": "Creare e gestire risorse accessibili al pubblico tramite un browser web",
|
"proxyResourceDescription": "Creare e gestire risorse pubbliche accessibili tramite un browser web",
|
||||||
"proxyResourcesBannerTitle": "Accesso Pubblico Basato sul Web",
|
"proxyResourcesBannerTitle": "Accesso Pubblico Basato sul Web",
|
||||||
"proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili a chiunque su Internet tramite un browser web. A differenza delle risorse private, non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.",
|
"proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili da chiunque tramite Internet da un browser web. A differenza delle risorse private non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.",
|
||||||
"clientResourceTitle": "Gestisci Risorse Private",
|
"clientResourceTitle": "Gestisci Risorse Private",
|
||||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||||
@@ -174,12 +174,12 @@
|
|||||||
"authentication": "Autenticazione",
|
"authentication": "Autenticazione",
|
||||||
"protected": "Protetto",
|
"protected": "Protetto",
|
||||||
"notProtected": "Non Protetto",
|
"notProtected": "Non Protetto",
|
||||||
"resourceMessageRemove": "Una volta rimossa, la risorsa non sarà più accessibile. Tutti gli obiettivi associati alla risorsa saranno rimossi.",
|
"resourceMessageRemove": "Una volta rimossa la risorsa non sarà più accessibile. Tutti gli oggetti target associati alla risorsa saranno rimossi.",
|
||||||
"resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?",
|
"resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?",
|
||||||
"resourceHTTP": "Risorsa HTTPS",
|
"resourceHTTP": "Risorsa HTTPS",
|
||||||
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
||||||
"resourceRaw": "Risorsa Raw TCP/UDP",
|
"resourceRaw": "Risorsa Raw TCP/UDP",
|
||||||
"resourceRawDescription": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta.",
|
"resourceRawDescription": "Richieste proxy su TCP/UDP raw utilizzando un numero di porta.",
|
||||||
"resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.",
|
"resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.",
|
||||||
"resourceCreate": "Crea Risorsa",
|
"resourceCreate": "Crea Risorsa",
|
||||||
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
"selectCountry": "Seleziona paese",
|
"selectCountry": "Seleziona paese",
|
||||||
"searchCountries": "Cerca paesi...",
|
"searchCountries": "Cerca paesi...",
|
||||||
"noCountryFound": "Nessun paese trovato.",
|
"noCountryFound": "Nessun paese trovato.",
|
||||||
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
||||||
"resourceType": "Tipo Di Risorsa",
|
"resourceType": "Tipo Di Risorsa",
|
||||||
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
||||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||||
@@ -206,13 +206,13 @@
|
|||||||
"protocol": "Protocollo",
|
"protocol": "Protocollo",
|
||||||
"protocolSelect": "Seleziona un protocollo",
|
"protocolSelect": "Seleziona un protocollo",
|
||||||
"resourcePortNumber": "Numero Porta",
|
"resourcePortNumber": "Numero Porta",
|
||||||
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.",
|
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste proxy.",
|
||||||
"back": "Indietro",
|
"back": "Indietro",
|
||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"resourceConfig": "Snippet Di Configurazione",
|
"resourceConfig": "Snippet Di Configurazione",
|
||||||
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
||||||
"resourceAddEntrypoints": "Traefik: Aggiungi Ingresso",
|
"resourceAddEntrypoints": "Traefik: Aggiungi Entrypoint",
|
||||||
"resourceExposePorts": "Gerbil: espone le porte in Docker componi",
|
"resourceExposePorts": "Gerbil: espone le porte in Docker Compose",
|
||||||
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
||||||
"resourceBack": "Torna alle risorse",
|
"resourceBack": "Torna alle risorse",
|
||||||
"resourceGoTo": "Vai alla Risorsa",
|
"resourceGoTo": "Vai alla Risorsa",
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
"rules": "Regole",
|
"rules": "Regole",
|
||||||
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
||||||
"resourceSetting": "Impostazioni {resourceName}",
|
"resourceSetting": "Impostazioni {resourceName}",
|
||||||
"alwaysAllow": "Autenticazione Bypass",
|
"alwaysAllow": "Bypass Autenticazione",
|
||||||
"alwaysDeny": "Blocca Accesso",
|
"alwaysDeny": "Blocca Accesso",
|
||||||
"passToAuth": "Passa all'autenticazione",
|
"passToAuth": "Passa all'autenticazione",
|
||||||
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
||||||
@@ -237,11 +237,11 @@
|
|||||||
"saveGeneralSettings": "Salva Impostazioni Generali",
|
"saveGeneralSettings": "Salva Impostazioni Generali",
|
||||||
"saveSettings": "Salva Impostazioni",
|
"saveSettings": "Salva Impostazioni",
|
||||||
"orgDangerZone": "Zona Pericolosa",
|
"orgDangerZone": "Zona Pericolosa",
|
||||||
"orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.",
|
"orgDangerZoneDescription": "Una volta che si elimina questa org non sarà possibile tornare indietro, assicurarsi quindi di essere certi della decisione.",
|
||||||
"orgDelete": "Elimina Organizzazione",
|
"orgDelete": "Elimina Organizzazione",
|
||||||
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
||||||
"orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.",
|
"orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.",
|
||||||
"orgMessageConfirm": "Per confermare, digita il nome dell'organizzazione qui sotto.",
|
"orgMessageConfirm": "Per confermare digita il nome dell'organizzazione qui sotto.",
|
||||||
"orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione?",
|
"orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione?",
|
||||||
"orgUpdated": "Organizzazione aggiornata",
|
"orgUpdated": "Organizzazione aggiornata",
|
||||||
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
||||||
@@ -254,10 +254,10 @@
|
|||||||
"orgDeleted": "Organizzazione eliminata",
|
"orgDeleted": "Organizzazione eliminata",
|
||||||
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
||||||
"deleteAccount": "Elimina Account",
|
"deleteAccount": "Elimina Account",
|
||||||
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.",
|
||||||
"deleteAccountButton": "Elimina Account",
|
"deleteAccountButton": "Elimina Account",
|
||||||
"deleteAccountConfirmTitle": "Elimina Account",
|
"deleteAccountConfirmTitle": "Elimina Account",
|
||||||
"deleteAccountConfirmMessage": "Questo cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
"deleteAccountConfirmMessage": "Questa operazione cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.",
|
||||||
"deleteAccountConfirmString": "elimina account",
|
"deleteAccountConfirmString": "elimina account",
|
||||||
"deleteAccountSuccess": "Account Eliminato",
|
"deleteAccountSuccess": "Account Eliminato",
|
||||||
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
"accessUserCreate": "Crea Utente",
|
"accessUserCreate": "Crea Utente",
|
||||||
"accessUserRemove": "Rimuovi Utente",
|
"accessUserRemove": "Rimuovi Utente",
|
||||||
"username": "Nome utente",
|
"username": "Nome utente",
|
||||||
"identityProvider": "Provider Di Identità",
|
"identityProvider": "Provider Identità",
|
||||||
"role": "Ruolo",
|
"role": "Ruolo",
|
||||||
"nameRequired": "Il nome è obbligatorio",
|
"nameRequired": "Il nome è obbligatorio",
|
||||||
"accessRolesManage": "Gestisci Ruoli",
|
"accessRolesManage": "Gestisci Ruoli",
|
||||||
@@ -328,8 +328,8 @@
|
|||||||
"apiKeysDelete": "Elimina Chiave API",
|
"apiKeysDelete": "Elimina Chiave API",
|
||||||
"apiKeysManage": "Gestisci Chiavi API",
|
"apiKeysManage": "Gestisci Chiavi API",
|
||||||
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
||||||
"provisioningKeysTitle": "Chiave Di Provvedimento",
|
"provisioningKeysTitle": "Chiave di provisioning",
|
||||||
"provisioningKeysManage": "Gestisci Chiavi Di Provvedimento",
|
"provisioningKeysManage": "Gestisci Chiavi di provisioning",
|
||||||
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
||||||
"provisioningManage": "Accantonamento",
|
"provisioningManage": "Accantonamento",
|
||||||
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
||||||
@@ -337,25 +337,25 @@
|
|||||||
"siteApproveSuccess": "Sito approvato con successo",
|
"siteApproveSuccess": "Sito approvato con successo",
|
||||||
"siteApproveError": "Errore nell'approvazione del sito",
|
"siteApproveError": "Errore nell'approvazione del sito",
|
||||||
"provisioningKeys": "Chiavi Di Provvedimento",
|
"provisioningKeys": "Chiavi Di Provvedimento",
|
||||||
"searchProvisioningKeys": "Cerca i tasti di provisioning ...",
|
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
|
||||||
"provisioningKeysAdd": "Genera Chiave Di Provvedimento",
|
"provisioningKeysAdd": "Genera Chiave di provisioning",
|
||||||
"provisioningKeysErrorDelete": "Errore nell'eliminare la chiave di provisioning",
|
"provisioningKeysErrorDelete": "Errore nell'eliminazione della chiave di provisioning",
|
||||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminare la chiave di provisioning",
|
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave di provisioning",
|
||||||
"provisioningKeysQuestionRemove": "Sei sicuro di voler rimuovere questa chiave di provisioning dall'organizzazione?",
|
"provisioningKeysQuestionRemove": "Sei sicuro di voler rimuovere questa chiave di provisioning dall'organizzazione?",
|
||||||
"provisioningKeysMessageRemove": "Una volta rimossa, la chiave non può più essere utilizzata per il provisioning.",
|
"provisioningKeysMessageRemove": "Una volta rimossa, la chiave non può più essere utilizzata per il provisioning.",
|
||||||
"provisioningKeysDeleteConfirm": "Conferma Elimina Chiave Provvisoria",
|
"provisioningKeysDeleteConfirm": "Conferma Eliminazione della chiave di provisioning",
|
||||||
"provisioningKeysDelete": "Elimina chiave di provisioning",
|
"provisioningKeysDelete": "Elimina chiave di provisioning",
|
||||||
"provisioningKeysCreate": "Genera Chiave Di Provvedimento",
|
"provisioningKeysCreate": "Genera Chiave di provisioning",
|
||||||
"provisioningKeysCreateDescription": "Genera una nuova chiave di provisioning per l'organizzazione",
|
"provisioningKeysCreateDescription": "Genera una nuova chiave di provisioning per l'organizzazione",
|
||||||
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
||||||
"provisioningKeysSave": "Salva la chiave di provisioning",
|
"provisioningKeysSave": "Salva la chiave di provisioning",
|
||||||
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
||||||
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
||||||
"provisioningKeysList": "Nuova chiave di provisioning",
|
"provisioningKeysList": "Nuova chiave di provisioning",
|
||||||
"provisioningKeysMaxBatchSize": "Dimensione massima lotto",
|
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
|
||||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del lotto (nessun limite)",
|
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
|
||||||
"provisioningKeysMaxBatchUnlimited": "Illimitato",
|
"provisioningKeysMaxBatchUnlimited": "Illimitato",
|
||||||
"provisioningKeysMaxBatchSizeInvalid": "Inserisci un lotto massimo valido (1–1.000.000).",
|
"provisioningKeysMaxBatchSizeInvalid": "Inserisci una dimensione massima valida del batch (1–1.000.000).",
|
||||||
"provisioningKeysValidUntil": "Valido fino al",
|
"provisioningKeysValidUntil": "Valido fino al",
|
||||||
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
||||||
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
||||||
@@ -363,14 +363,14 @@
|
|||||||
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
||||||
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
||||||
"provisioningKeysNeverUsed": "Mai",
|
"provisioningKeysNeverUsed": "Mai",
|
||||||
"provisioningKeysEdit": "Modifica Chiave Di Provvedimento",
|
"provisioningKeysEdit": "Modifica Chiave di provisioning",
|
||||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del lotto e il tempo di scadenza per questa chiave.",
|
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
|
||||||
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
||||||
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
||||||
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
|
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
|
||||||
"provisioningKeysUpdated": "Chiave di accantonamento aggiornata",
|
"provisioningKeysUpdated": "Chiave di provisioning aggiornata",
|
||||||
"provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.",
|
"provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.",
|
||||||
"provisioningKeysBannerTitle": "Chiavi Di Provvedimento Sito",
|
"provisioningKeysBannerTitle": "Chiavi di provisioning del Sito",
|
||||||
"provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.",
|
"provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.",
|
||||||
"provisioningKeysBannerButtonText": "Scopri di più",
|
"provisioningKeysBannerButtonText": "Scopri di più",
|
||||||
"pendingSitesBannerTitle": "Siti In Attesa",
|
"pendingSitesBannerTitle": "Siti In Attesa",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
"userErrorDelete": "Errore nell'eliminare l'utente",
|
"userErrorDelete": "Errore nell'eliminare l'utente",
|
||||||
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
||||||
"userDeleteServer": "Elimina utente dal server",
|
"userDeleteServer": "Elimina utente dal server",
|
||||||
"userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni ed essere completamente rimosso dal server.",
|
"userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni e verrà completamente rimosso dal server.",
|
||||||
"userQuestionRemove": "Sei sicuro di voler eliminare definitivamente l'utente dal server?",
|
"userQuestionRemove": "Sei sicuro di voler eliminare definitivamente l'utente dal server?",
|
||||||
"licenseKey": "Chiave Di Licenza",
|
"licenseKey": "Chiave Di Licenza",
|
||||||
"valid": "Valido",
|
"valid": "Valido",
|
||||||
@@ -404,9 +404,9 @@
|
|||||||
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
||||||
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
||||||
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
||||||
"licenseAbout": "Informazioni Su Licenze",
|
"licenseAbout": "Informazioni sul Licensing",
|
||||||
"communityEdition": "Edizione Community",
|
"communityEdition": "Edizione Community",
|
||||||
"licenseAboutDescription": "Questo è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.",
|
"licenseAboutDescription": "Questa sezione è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.",
|
||||||
"licenseKeyActivated": "Chiave di licenza attivata",
|
"licenseKeyActivated": "Chiave di licenza attivata",
|
||||||
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
||||||
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
||||||
@@ -429,7 +429,7 @@
|
|||||||
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
||||||
"licensedNot": "Non Licenziato",
|
"licensedNot": "Non Licenziato",
|
||||||
"hostId": "ID Host",
|
"hostId": "ID Host",
|
||||||
"licenseReckeckAll": "Ricontrolla Tutte Le Tasti",
|
"licenseReckeckAll": "Ricontrolla Tutte le chiavi",
|
||||||
"licenseSiteUsage": "Utilizzo Siti",
|
"licenseSiteUsage": "Utilizzo Siti",
|
||||||
"licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.",
|
"licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.",
|
||||||
"licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.",
|
"licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.",
|
||||||
@@ -480,7 +480,7 @@
|
|||||||
"userOrgRemoved": "Utente rimosso",
|
"userOrgRemoved": "Utente rimosso",
|
||||||
"userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.",
|
"userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.",
|
||||||
"userQuestionOrgRemove": "Sei sicuro di voler rimuovere questo utente dall'organizzazione?",
|
"userQuestionOrgRemove": "Sei sicuro di voler rimuovere questo utente dall'organizzazione?",
|
||||||
"userMessageOrgRemove": "Una volta rimosso, questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||||
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
@@ -532,13 +532,13 @@
|
|||||||
"approve": "Approva",
|
"approve": "Approva",
|
||||||
"approved": "Approvato",
|
"approved": "Approvato",
|
||||||
"denied": "Negato",
|
"denied": "Negato",
|
||||||
"deniedApproval": "Omologazione Negata",
|
"deniedApproval": "Approvazione Negata",
|
||||||
"all": "Tutti",
|
"all": "Tutti",
|
||||||
"deny": "Nega",
|
"deny": "Nega",
|
||||||
"viewDetails": "Visualizza Dettagli",
|
"viewDetails": "Visualizza Dettagli",
|
||||||
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
||||||
"resetFilters": "Ripristina Filtri",
|
"resetFilters": "Ripristina Filtri",
|
||||||
"totalBlocked": "Richieste Bloccate Da Pangolino",
|
"totalBlocked": "Richieste Bloccate Da Pangolin",
|
||||||
"totalRequests": "Totale Richieste",
|
"totalRequests": "Totale Richieste",
|
||||||
"requestsByCountry": "Richieste Per Paese",
|
"requestsByCountry": "Richieste Per Paese",
|
||||||
"requestsByDay": "Richieste Per Giorno",
|
"requestsByDay": "Richieste Per Giorno",
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
"allowed": "Consentito",
|
"allowed": "Consentito",
|
||||||
"topCountries": "Paesi Principali",
|
"topCountries": "Paesi Principali",
|
||||||
"accessRoleSelect": "Seleziona ruolo",
|
"accessRoleSelect": "Seleziona ruolo",
|
||||||
"inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.",
|
"inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. L'utente deve accedere al link per accettare l'invito.",
|
||||||
"inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.",
|
"inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.",
|
||||||
"inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
|
"inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
|
||||||
"idpTitle": "Informazioni Generali",
|
"idpTitle": "Informazioni Generali",
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
"userSaved": "Utente salvato",
|
"userSaved": "Utente salvato",
|
||||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||||
"autoProvisioned": "Auto Provisioned",
|
"autoProvisioned": "Auto Provisioned",
|
||||||
"autoProvisionSettings": "Impostazioni Automatiche Di Fornitura",
|
"autoProvisionSettings": "Impostazioni Automatiche di provisioning",
|
||||||
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
||||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||||
"accessControlsSubmit": "Salva Controlli di Accesso",
|
"accessControlsSubmit": "Salva Controlli di Accesso",
|
||||||
@@ -576,9 +576,9 @@
|
|||||||
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
|
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
|
||||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||||
"proxyEnableSSL": "Abilita SSL",
|
"proxyEnableSSL": "Abilita SSL",
|
||||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure agli obiettivi.",
|
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.",
|
||||||
"target": "Target",
|
"target": "Target",
|
||||||
"configureTarget": "Configura Obiettivi",
|
"configureTarget": "Configura Risorse Interne",
|
||||||
"targetErrorFetch": "Impossibile recuperare i target",
|
"targetErrorFetch": "Impossibile recuperare i target",
|
||||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||||
"domainPickerVerified": "Verificato",
|
"domainPickerVerified": "Verificato",
|
||||||
"domainPickerUnverified": "Non Verificato",
|
"domainPickerUnverified": "Non Verificato",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
||||||
"domainPickerError": "Errore",
|
"domainPickerError": "Errore",
|
||||||
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||||
"domainPickerVerified": "검증됨",
|
"domainPickerVerified": "검증됨",
|
||||||
"domainPickerUnverified": "검증되지 않음",
|
"domainPickerUnverified": "검증되지 않음",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
||||||
"domainPickerError": "오류",
|
"domainPickerError": "오류",
|
||||||
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
||||||
"domainPickerVerified": "Bekreftet",
|
"domainPickerVerified": "Bekreftet",
|
||||||
"domainPickerUnverified": "Uverifisert",
|
"domainPickerUnverified": "Uverifisert",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
||||||
"domainPickerError": "Feil",
|
"domainPickerError": "Feil",
|
||||||
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||||
"domainPickerVerified": "Geverifieerd",
|
"domainPickerVerified": "Geverifieerd",
|
||||||
"domainPickerUnverified": "Ongeverifieerd",
|
"domainPickerUnverified": "Ongeverifieerd",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
||||||
"domainPickerError": "Foutmelding",
|
"domainPickerError": "Foutmelding",
|
||||||
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||||
"domainPickerVerified": "Zweryfikowano",
|
"domainPickerVerified": "Zweryfikowano",
|
||||||
"domainPickerUnverified": "Niezweryfikowane",
|
"domainPickerUnverified": "Niezweryfikowane",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
||||||
"domainPickerError": "Błąd",
|
"domainPickerError": "Błąd",
|
||||||
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||||
"domainPickerVerified": "Verificada",
|
"domainPickerVerified": "Verificada",
|
||||||
"domainPickerUnverified": "Não verificado",
|
"domainPickerUnverified": "Não verificado",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
||||||
"domainPickerError": "ERRO",
|
"domainPickerError": "ERRO",
|
||||||
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||||
"domainPickerVerified": "Подтверждено",
|
"domainPickerVerified": "Подтверждено",
|
||||||
"domainPickerUnverified": "Не подтверждено",
|
"domainPickerUnverified": "Не подтверждено",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
||||||
"domainPickerError": "Ошибка",
|
"domainPickerError": "Ошибка",
|
||||||
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
||||||
"domainPickerVerified": "Doğrulandı",
|
"domainPickerVerified": "Doğrulandı",
|
||||||
"domainPickerUnverified": "Doğrulanmadı",
|
"domainPickerUnverified": "Doğrulanmadı",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
||||||
"domainPickerError": "Hata",
|
"domainPickerError": "Hata",
|
||||||
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
||||||
|
|||||||
@@ -2116,6 +2116,7 @@
|
|||||||
"domainPickerFreeProvidedDomain": "免费提供的域",
|
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||||
"domainPickerVerified": "已验证",
|
"domainPickerVerified": "已验证",
|
||||||
"domainPickerUnverified": "未验证",
|
"domainPickerUnverified": "未验证",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
||||||
"domainPickerError": "错误",
|
"domainPickerError": "错误",
|
||||||
"domainPickerErrorLoadDomains": "加载组织域名失败",
|
"domainPickerErrorLoadDomains": "加载组织域名失败",
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ export const orgs = pgTable("orgs", {
|
|||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
settingsLogRetentionDaysConnection: integer(
|
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
"settingsLogRetentionDaysConnection"
|
|
||||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
@@ -103,9 +101,7 @@ export const sites = pgTable("sites", {
|
|||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||||
status: varchar("status")
|
status: varchar("status").$type<"pending" | "approved">().default("approved")
|
||||||
.$type<"pending" | "approved">()
|
|
||||||
.default("approved")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
@@ -234,9 +230,8 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
ssl: boolean("ssl").notNull().default(false),
|
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
protocol: varchar("protocol"), // only for port mode
|
||||||
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
settingsLogRetentionDaysConnection: integer(
|
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
"settingsLogRetentionDaysConnection"
|
|
||||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
@@ -260,9 +258,8 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
protocol: text("protocol"), // only for port mode
|
||||||
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
destination: text("destination").notNull(), // ip, cidr, hostname
|
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
|||||||
import { initCleanup } from "#dynamic/cleanup";
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
import license from "#dynamic/license/license";
|
import license from "#dynamic/license/license";
|
||||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
|
||||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
@@ -40,7 +39,6 @@ async function startServers() {
|
|||||||
initTelemetryClient();
|
initTelemetryClient();
|
||||||
|
|
||||||
initLogCleanupInterval();
|
initLogCleanupInterval();
|
||||||
initAcmeCertSync();
|
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function initAcmeCertSync(): void {
|
|
||||||
// stub
|
|
||||||
}
|
|
||||||
@@ -16,20 +16,6 @@ import { Config } from "./types";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
|
|
||||||
function siteResourceModeForDb(mode: "host" | "cidr" | "http" | "https"): {
|
|
||||||
mode: "host" | "cidr" | "http";
|
|
||||||
ssl: boolean;
|
|
||||||
scheme: "http" | "https" | null;
|
|
||||||
} {
|
|
||||||
if (mode === "https") {
|
|
||||||
return { mode: "http", ssl: true, scheme: "https" };
|
|
||||||
}
|
|
||||||
if (mode === "http") {
|
|
||||||
return { mode: "http", ssl: false, scheme: "http" };
|
|
||||||
}
|
|
||||||
return { mode, ssl: false, scheme: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClientResourcesResults = {
|
export type ClientResourcesResults = {
|
||||||
newSiteResource: SiteResource;
|
newSiteResource: SiteResource;
|
||||||
oldSiteResource?: SiteResource;
|
oldSiteResource?: SiteResource;
|
||||||
@@ -90,18 +76,14 @@ export async function updateClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingResource) {
|
if (existingResource) {
|
||||||
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
const [updatedResource] = await trx
|
const [updatedResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
mode: mappedMode.mode,
|
mode: resourceData.mode,
|
||||||
ssl: mappedMode.ssl,
|
|
||||||
scheme: mappedMode.scheme,
|
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
|
||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
// enabled: resourceData.enabled ?? true,
|
// enabled: resourceData.enabled ?? true,
|
||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
@@ -225,9 +207,9 @@ export async function updateClientResources(
|
|||||||
oldSiteResource: existingResource
|
oldSiteResource: existingResource
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
if (mappedMode.mode === "host" || mappedMode.mode === "http") {
|
if (resourceData.mode == "host") {
|
||||||
|
// we can only have an alias on a host
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,11 +221,8 @@ export async function updateClientResources(
|
|||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
niceId: resourceNiceId,
|
niceId: resourceNiceId,
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
mode: mappedMode.mode,
|
mode: resourceData.mode,
|
||||||
ssl: mappedMode.ssl,
|
|
||||||
scheme: mappedMode.scheme,
|
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
|
||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
// enabled: resourceData.enabled ?? true,
|
// enabled: resourceData.enabled ?? true,
|
||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
|
|||||||
@@ -325,11 +325,11 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
|||||||
export const ClientResourceSchema = z
|
export const ClientResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http", "https"]),
|
mode: z.enum(["host", "cidr"]),
|
||||||
site: z.string(),
|
site: z.string(),
|
||||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
// proxyPort: z.int().positive().optional(),
|
// proxyPort: z.int().positive().optional(),
|
||||||
"destination-port": z.int().positive().optional(),
|
// destinationPort: z.int().positive().optional(),
|
||||||
destination: z.string().min(1),
|
destination: z.string().min(1),
|
||||||
// enabled: z.boolean().default(true),
|
// enabled: z.boolean().default(true),
|
||||||
"tcp-ports": portRangeStringSchema.optional().default("*"),
|
"tcp-ports": portRangeStringSchema.optional().default("*"),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import config from "@server/lib/config";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { getValidCertificatesForDomains } from "#private/lib/certificates";
|
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
start: bigint;
|
start: bigint;
|
||||||
@@ -583,26 +582,16 @@ export type SubnetProxyTargetV2 = {
|
|||||||
protocol: "tcp" | "udp";
|
protocol: "tcp" | "udp";
|
||||||
}[];
|
}[];
|
||||||
resourceId?: number;
|
resourceId?: number;
|
||||||
protocol?: "http" | "https"; // if set, this target only applies to the specified protocol
|
|
||||||
httpTargets?: HTTPTarget[];
|
|
||||||
tlsCert?: string;
|
|
||||||
tlsKey?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HTTPTarget = {
|
export function generateSubnetProxyTargetV2(
|
||||||
destAddr: string; // must be an IP or hostname
|
|
||||||
destPort: number;
|
|
||||||
scheme: "http" | "https";
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateSubnetProxyTargetV2(
|
|
||||||
siteResource: SiteResource,
|
siteResource: SiteResource,
|
||||||
clients: {
|
clients: {
|
||||||
clientId: number;
|
clientId: number;
|
||||||
pubKey: string | null;
|
pubKey: string | null;
|
||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
}[]
|
}[]
|
||||||
): Promise<SubnetProxyTargetV2 | undefined> {
|
): SubnetProxyTargetV2 | undefined {
|
||||||
if (clients.length === 0) {
|
if (clients.length === 0) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
||||||
@@ -630,7 +619,7 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
destPrefix: destination,
|
destPrefix: destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId
|
resourceId: siteResource.siteResourceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +631,7 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
rewriteTo: destination,
|
rewriteTo: destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId
|
resourceId: siteResource.siteResourceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (siteResource.mode == "cidr") {
|
} else if (siteResource.mode == "cidr") {
|
||||||
@@ -651,69 +640,7 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
destPrefix: siteResource.destination,
|
destPrefix: siteResource.destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId
|
|
||||||
};
|
|
||||||
} else if (siteResource.mode == "http") {
|
|
||||||
let destination = siteResource.destination;
|
|
||||||
// check if this is a valid ip
|
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
|
||||||
if (ipSchema.safeParse(destination).success) {
|
|
||||||
destination = `${destination}/32`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!siteResource.alias ||
|
|
||||||
!siteResource.aliasAddress ||
|
|
||||||
!siteResource.destinationPort ||
|
|
||||||
!siteResource.scheme
|
|
||||||
) {
|
|
||||||
logger.debug(
|
|
||||||
`Site resource ${siteResource.siteResourceId} is in HTTP mode but is missing alias or alias address or destinationPort or scheme, skipping alias target generation.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const publicProtocol = siteResource.ssl ? "https" : "http";
|
|
||||||
// also push a match for the alias address
|
|
||||||
let tlsCert: string | undefined;
|
|
||||||
let tlsKey: string | undefined;
|
|
||||||
|
|
||||||
if (siteResource.ssl && siteResource.alias) {
|
|
||||||
try {
|
|
||||||
const certs = await getValidCertificatesForDomains(
|
|
||||||
new Set([siteResource.alias]),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (certs.length > 0 && certs[0].certFile && certs[0].keyFile) {
|
|
||||||
tlsCert = certs[0].certFile;
|
|
||||||
tlsKey = certs[0].keyFile;
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
`No valid certificate found for SSL site resource ${siteResource.siteResourceId} with domain ${siteResource.alias}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to retrieve certificate for site resource ${siteResource.siteResourceId} domain ${siteResource.alias}: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target = {
|
|
||||||
sourcePrefixes: [],
|
|
||||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
|
||||||
rewriteTo: destination,
|
|
||||||
portRange,
|
|
||||||
disableIcmp,
|
|
||||||
resourceId: siteResource.siteResourceId,
|
resourceId: siteResource.siteResourceId,
|
||||||
protocol: siteResource.ssl ? "https" : "http",
|
|
||||||
httpTargets: [
|
|
||||||
{
|
|
||||||
destAddr: siteResource.destination,
|
|
||||||
destPort: siteResource.destinationPort,
|
|
||||||
scheme: siteResource.scheme
|
|
||||||
}
|
|
||||||
],
|
|
||||||
...(tlsCert && tlsKey ? { tlsCert, tlsKey } : {})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,15 +670,16 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
||||||
* by expanding each source prefix into its own target entry.
|
* by expanding each source prefix into its own target entry.
|
||||||
* @param targetV2 - The v2 target to convert
|
* @param targetV2 - The v2 target to convert
|
||||||
* @returns Array of v1 SubnetProxyTarget objects
|
* @returns Array of v1 SubnetProxyTarget objects
|
||||||
*/
|
*/
|
||||||
export function convertSubnetProxyTargetsV2ToV1(
|
export function convertSubnetProxyTargetsV2ToV1(
|
||||||
targetsV2: SubnetProxyTargetV2[]
|
targetsV2: SubnetProxyTargetV2[]
|
||||||
): SubnetProxyTarget[] {
|
): SubnetProxyTarget[] {
|
||||||
return targetsV2.flatMap((targetV2) =>
|
return targetsV2.flatMap((targetV2) =>
|
||||||
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
||||||
sourcePrefix,
|
sourcePrefix,
|
||||||
@@ -767,7 +695,8 @@ export function convertSubnetProxyTargetsV2ToV1(
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Custom schema for validating port range strings
|
// Custom schema for validating port range strings
|
||||||
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (addedClients.length > 0) {
|
if (addedClients.length > 0) {
|
||||||
const targetToAdd = await generateSubnetProxyTargetV2(
|
const targetToAdd = generateSubnetProxyTargetV2(
|
||||||
siteResource,
|
siteResource,
|
||||||
addedClients
|
addedClients
|
||||||
);
|
);
|
||||||
@@ -698,7 +698,7 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (removedClients.length > 0) {
|
if (removedClients.length > 0) {
|
||||||
const targetToRemove = await generateSubnetProxyTargetV2(
|
const targetToRemove = generateSubnetProxyTargetV2(
|
||||||
siteResource,
|
siteResource,
|
||||||
removedClients
|
removedClients
|
||||||
);
|
);
|
||||||
@@ -1164,7 +1164,7 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const target = await generateSubnetProxyTargetV2(resource, [
|
const target = generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
pubKey: client.pubKey,
|
pubKey: client.pubKey,
|
||||||
@@ -1241,7 +1241,7 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const target = await generateSubnetProxyTargetV2(resource, [
|
const target = generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
pubKey: client.pubKey,
|
pubKey: client.pubKey,
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { certificates, domains, db } from "@server/db";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import { encryptData, decryptData } from "@server/lib/encryption";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import config from "#private/lib/config";
|
|
||||||
|
|
||||||
interface AcmeCert {
|
|
||||||
domain: { main: string; sans?: string[] };
|
|
||||||
certificate: string;
|
|
||||||
key: string;
|
|
||||||
Store: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AcmeJson {
|
|
||||||
[resolver: string]: {
|
|
||||||
Certificates: AcmeCert[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEncryptionKey(): Buffer {
|
|
||||||
const keyHex = config.getRawPrivateConfig().server.encryption_key;
|
|
||||||
if (!keyHex) {
|
|
||||||
throw new Error("acmeCertSync: encryption key is not configured");
|
|
||||||
}
|
|
||||||
return Buffer.from(keyHex, "hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findDomainId(certDomain: string): Promise<string | null> {
|
|
||||||
// Strip wildcard prefix before lookup (*.example.com -> example.com)
|
|
||||||
const lookupDomain = certDomain.startsWith("*.")
|
|
||||||
? certDomain.slice(2)
|
|
||||||
: certDomain;
|
|
||||||
|
|
||||||
// 1. Exact baseDomain match (any domain type)
|
|
||||||
const exactMatch = await db
|
|
||||||
.select({ domainId: domains.domainId })
|
|
||||||
.from(domains)
|
|
||||||
.where(eq(domains.baseDomain, lookupDomain))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (exactMatch.length > 0) {
|
|
||||||
return exactMatch[0].domainId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Walk up the domain hierarchy looking for a wildcard-type domain whose
|
|
||||||
// baseDomain is a suffix of the cert domain. e.g. cert "sub.example.com"
|
|
||||||
// matches a wildcard domain with baseDomain "example.com".
|
|
||||||
const parts = lookupDomain.split(".");
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
|
||||||
const candidate = parts.slice(i).join(".");
|
|
||||||
if (!candidate) continue;
|
|
||||||
|
|
||||||
const wildcardMatch = await db
|
|
||||||
.select({ domainId: domains.domainId })
|
|
||||||
.from(domains)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(domains.baseDomain, candidate),
|
|
||||||
eq(domains.type, "wildcard")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (wildcardMatch.length > 0) {
|
|
||||||
return wildcardMatch[0].domainId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFirstCert(pemBundle: string): string | null {
|
|
||||||
const match = pemBundle.match(
|
|
||||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/
|
|
||||||
);
|
|
||||||
return match ? match[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncAcmeCerts(
|
|
||||||
acmeJsonPath: string,
|
|
||||||
resolver: string
|
|
||||||
): Promise<void> {
|
|
||||||
let raw: string;
|
|
||||||
try {
|
|
||||||
raw = fs.readFileSync(acmeJsonPath, "utf8");
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not read ${acmeJsonPath}: ${err}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let acmeJson: AcmeJson;
|
|
||||||
try {
|
|
||||||
acmeJson = JSON.parse(raw);
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(`acmeCertSync: could not parse acme.json: ${err}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolverData = acmeJson[resolver];
|
|
||||||
if (!resolverData || !Array.isArray(resolverData.Certificates)) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: no certificates found for resolver "${resolver}"`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionKey = getEncryptionKey();
|
|
||||||
|
|
||||||
for (const cert of resolverData.Certificates) {
|
|
||||||
const domain = cert.domain?.main;
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: skipping cert with missing domain`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cert.certificate || !cert.key) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: skipping cert for ${domain} - empty certificate or key field`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const certPem = Buffer.from(cert.certificate, "base64").toString(
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
const keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
|
||||||
|
|
||||||
if (!certPem.trim() || !keyPem.trim()) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: skipping cert for ${domain} - blank PEM after base64 decode`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if cert already exists in DB
|
|
||||||
const existing = await db
|
|
||||||
.select()
|
|
||||||
.from(certificates)
|
|
||||||
.where(eq(certificates.domain, domain))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existing.length > 0 && existing[0].certFile) {
|
|
||||||
try {
|
|
||||||
const storedCertPem = decryptData(
|
|
||||||
existing[0].certFile,
|
|
||||||
encryptionKey
|
|
||||||
);
|
|
||||||
if (storedCertPem === certPem) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: cert for ${domain} is unchanged, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Decryption failure means we should proceed with the update
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse cert expiry from the first cert in the PEM bundle
|
|
||||||
let expiresAt: number | null = null;
|
|
||||||
const firstCertPem = extractFirstCert(certPem);
|
|
||||||
if (firstCertPem) {
|
|
||||||
try {
|
|
||||||
const x509 = new crypto.X509Certificate(firstCertPem);
|
|
||||||
expiresAt = Math.floor(
|
|
||||||
new Date(x509.validTo).getTime() / 1000
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wildcard = domain.startsWith("*.");
|
|
||||||
const encryptedCert = encryptData(certPem, encryptionKey);
|
|
||||||
const encryptedKey = encryptData(keyPem, encryptionKey);
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
const domainId = await findDomainId(domain);
|
|
||||||
if (domainId) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing.length > 0) {
|
|
||||||
await db
|
|
||||||
.update(certificates)
|
|
||||||
.set({
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard,
|
|
||||||
...(domainId !== null && { domainId })
|
|
||||||
})
|
|
||||||
.where(eq(certificates.domain, domain));
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await db.insert(certificates).values({
|
|
||||||
domain,
|
|
||||||
domainId,
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initAcmeCertSync(): void {
|
|
||||||
const privateConfig = config.getRawPrivateConfig();
|
|
||||||
|
|
||||||
if (!privateConfig.flags?.enable_acme_cert_sync) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acmeJsonPath =
|
|
||||||
privateConfig.acme?.acme_json_path ?? "config/letsencrypt/acme.json";
|
|
||||||
const resolver = privateConfig.acme?.resolver ?? "letsencrypt";
|
|
||||||
const intervalMs = privateConfig.acme?.sync_interval_ms ?? 5000;
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`acmeCertSync: starting ACME cert sync from "${acmeJsonPath}" using resolver "${resolver}" every ${intervalMs}ms`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run immediately on init, then on the configured interval
|
|
||||||
syncAcmeCerts(acmeJsonPath, resolver).catch((err) => {
|
|
||||||
logger.error(`acmeCertSync: error during initial sync: ${err}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
syncAcmeCerts(acmeJsonPath, resolver).catch((err) => {
|
|
||||||
logger.error(`acmeCertSync: error during sync: ${err}`);
|
|
||||||
});
|
|
||||||
}, intervalMs);
|
|
||||||
}
|
|
||||||
@@ -95,21 +95,10 @@ export const privateConfigSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
enable_redis: z.boolean().optional().default(false),
|
enable_redis: z.boolean().optional().default(false),
|
||||||
use_pangolin_dns: z.boolean().optional().default(false),
|
use_pangolin_dns: z.boolean().optional().default(false),
|
||||||
use_org_only_idp: z.boolean().optional(),
|
use_org_only_idp: z.boolean().optional()
|
||||||
enable_acme_cert_sync: z.boolean().optional().default(false)
|
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
acme: z
|
|
||||||
.object({
|
|
||||||
acme_json_path: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("config/letsencrypt/acme.json"),
|
|
||||||
resolver: z.string().optional().default("letsencrypt"),
|
|
||||||
sync_interval_ms: z.number().optional().default(5000)
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
branding: z
|
branding: z
|
||||||
.object({
|
.object({
|
||||||
app_name: z.string().optional(),
|
app_name: z.string().optional(),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { orgs, resources, sites, siteResources, Target, targets } from "@server/db";
|
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||||
import {
|
import {
|
||||||
sanitize,
|
sanitize,
|
||||||
encodePath,
|
encodePath,
|
||||||
@@ -267,34 +267,6 @@ export async function getTraefikConfig(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query siteResources in HTTP mode with SSL enabled and aliases — cert generation / HTTPS edge
|
|
||||||
const siteResourcesWithAliases = await db
|
|
||||||
.select({
|
|
||||||
siteResourceId: siteResources.siteResourceId,
|
|
||||||
alias: siteResources.alias,
|
|
||||||
mode: siteResources.mode
|
|
||||||
})
|
|
||||||
.from(siteResources)
|
|
||||||
.innerJoin(sites, eq(sites.siteId, siteResources.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(siteResources.enabled, true),
|
|
||||||
isNotNull(siteResources.alias),
|
|
||||||
eq(siteResources.mode, "http"),
|
|
||||||
eq(siteResources.ssl, true),
|
|
||||||
or(
|
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
|
||||||
and(
|
|
||||||
isNull(sites.exitNodeId),
|
|
||||||
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
|
|
||||||
eq(sites.type, "local"),
|
|
||||||
sql`(${build != "saas" ? 1 : 0} = 1)`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
inArray(sites.type, siteTypes)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let validCerts: CertificateResult[] = [];
|
let validCerts: CertificateResult[] = [];
|
||||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||||
// create a list of all domains to get certs for
|
// create a list of all domains to get certs for
|
||||||
@@ -304,12 +276,6 @@ export async function getTraefikConfig(
|
|||||||
domains.add(resource.fullDomain);
|
domains.add(resource.fullDomain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Include siteResource aliases so pangolin-dns also fetches certs for them
|
|
||||||
for (const sr of siteResourcesWithAliases) {
|
|
||||||
if (sr.alias) {
|
|
||||||
domains.add(sr.alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// get the valid certs for these domains
|
// get the valid certs for these domains
|
||||||
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
||||||
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
|
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
|
||||||
@@ -901,128 +867,6 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
|
|
||||||
// Traefik generates TLS certificates for those domains even when no
|
|
||||||
// matching resource exists yet.
|
|
||||||
if (siteResourcesWithAliases.length > 0) {
|
|
||||||
// Build a set of domains already covered by normal resources
|
|
||||||
const existingFullDomains = new Set<string>();
|
|
||||||
for (const resource of resourcesMap.values()) {
|
|
||||||
if (resource.fullDomain) {
|
|
||||||
existingFullDomains.add(resource.fullDomain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const sr of siteResourcesWithAliases) {
|
|
||||||
if (!sr.alias) continue;
|
|
||||||
|
|
||||||
// Skip if this alias is already handled by a resource router
|
|
||||||
if (existingFullDomains.has(sr.alias)) continue;
|
|
||||||
|
|
||||||
const alias = sr.alias;
|
|
||||||
const srKey = `site-resource-cert-${sr.siteResourceId}`;
|
|
||||||
const siteResourceServiceName = `${srKey}-service`;
|
|
||||||
const siteResourceRouterName = `${srKey}-router`;
|
|
||||||
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
|
|
||||||
|
|
||||||
const maintenancePort = config.getRawConfig().server.next_port;
|
|
||||||
const maintenanceHost =
|
|
||||||
config.getRawConfig().server.internal_hostname;
|
|
||||||
|
|
||||||
if (!config_output.http.routers) {
|
|
||||||
config_output.http.routers = {};
|
|
||||||
}
|
|
||||||
if (!config_output.http.services) {
|
|
||||||
config_output.http.services = {};
|
|
||||||
}
|
|
||||||
if (!config_output.http.middlewares) {
|
|
||||||
config_output.http.middlewares = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service pointing at the internal maintenance/Next.js page
|
|
||||||
config_output.http.services[siteResourceServiceName] = {
|
|
||||||
loadBalancer: {
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
passHostHeader: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Middleware that rewrites any path to /maintenance-screen
|
|
||||||
config_output.http.middlewares[
|
|
||||||
siteResourceRewriteMiddlewareName
|
|
||||||
] = {
|
|
||||||
replacePathRegex: {
|
|
||||||
regex: "^/(.*)",
|
|
||||||
replacement: "/private-maintenance-screen"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// HTTP -> HTTPS redirect so the ACME challenge can be served
|
|
||||||
config_output.http.routers[
|
|
||||||
`${siteResourceRouterName}-redirect`
|
|
||||||
] = {
|
|
||||||
entryPoints: [
|
|
||||||
config.getRawConfig().traefik.http_entrypoint
|
|
||||||
],
|
|
||||||
middlewares: [redirectHttpsMiddlewareName],
|
|
||||||
service: siteResourceServiceName,
|
|
||||||
rule: `Host(\`${alias}\`)`,
|
|
||||||
priority: 100
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine TLS / cert-resolver configuration
|
|
||||||
let tls: any = {};
|
|
||||||
if (
|
|
||||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
|
||||||
) {
|
|
||||||
const domainParts = alias.split(".");
|
|
||||||
const wildCard =
|
|
||||||
domainParts.length <= 2
|
|
||||||
? `*.${domainParts.join(".")}`
|
|
||||||
: `*.${domainParts.slice(1).join(".")}`;
|
|
||||||
|
|
||||||
const globalDefaultResolver =
|
|
||||||
config.getRawConfig().traefik.cert_resolver;
|
|
||||||
const globalDefaultPreferWildcard =
|
|
||||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
|
||||||
|
|
||||||
tls = {
|
|
||||||
certResolver: globalDefaultResolver,
|
|
||||||
...(globalDefaultPreferWildcard
|
|
||||||
? { domains: [{ main: wildCard }] }
|
|
||||||
: {})
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// pangolin-dns: only add route if we already have a valid cert
|
|
||||||
const matchingCert = validCerts.find(
|
|
||||||
(cert) => cert.queriedDomain === alias
|
|
||||||
);
|
|
||||||
if (!matchingCert) {
|
|
||||||
logger.debug(
|
|
||||||
`No matching certificate found for siteResource alias: ${alias}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPS router — presence of this entry triggers cert generation
|
|
||||||
config_output.http.routers[siteResourceRouterName] = {
|
|
||||||
entryPoints: [
|
|
||||||
config.getRawConfig().traefik.https_entrypoint
|
|
||||||
],
|
|
||||||
service: siteResourceServiceName,
|
|
||||||
middlewares: [siteResourceRewriteMiddlewareName],
|
|
||||||
rule: `Host(\`${alias}\`)`,
|
|
||||||
priority: 100,
|
|
||||||
tls
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (generateLoginPageRouters) {
|
if (generateLoginPageRouters) {
|
||||||
const exitNodeLoginPages = await db
|
const exitNodeLoginPages = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export async function buildClientConfigurationForNewtClient(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const resourceTarget = await generateSubnetProxyTargetV2(
|
const resourceTarget = generateSubnetProxyTargetV2(
|
||||||
resource,
|
resource,
|
||||||
resourceClients
|
resourceClients
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export async function getUserResources(
|
|||||||
name: string;
|
name: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
scheme: string | null;
|
protocol: string | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
@@ -156,7 +156,7 @@ export async function getUserResources(
|
|||||||
name: siteResources.name,
|
name: siteResources.name,
|
||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
scheme: siteResources.scheme,
|
protocol: siteResources.protocol,
|
||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
alias: siteResources.alias,
|
alias: siteResources.alias,
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
@@ -240,7 +240,7 @@ export async function getUserResources(
|
|||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
mode: siteResource.mode,
|
mode: siteResource.mode,
|
||||||
protocol: siteResource.scheme,
|
protocol: siteResource.protocol,
|
||||||
enabled: siteResource.enabled,
|
enabled: siteResource.enabled,
|
||||||
alias: siteResource.alias,
|
alias: siteResource.alias,
|
||||||
aliasAddress: siteResource.aliasAddress,
|
aliasAddress: siteResource.aliasAddress,
|
||||||
@@ -289,7 +289,7 @@ export type GetUserResourcesResponse = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
type: "site";
|
type: 'site';
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ const createSiteResourceParamsSchema = z.strictObject({
|
|||||||
const createSiteResourceSchema = z
|
const createSiteResourceSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http"]),
|
mode: z.enum(["host", "cidr", "port"]),
|
||||||
ssl: z.boolean().optional(), // only used for http mode
|
|
||||||
siteId: z.int(),
|
siteId: z.int(),
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
// proxyPort: z.int().positive().optional(),
|
// proxyPort: z.int().positive().optional(),
|
||||||
destinationPort: z.int().positive().optional(),
|
// destinationPort: z.int().positive().optional(),
|
||||||
destination: z.string().min(1),
|
destination: z.string().min(1),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
alias: z
|
alias: z
|
||||||
@@ -63,11 +62,7 @@ const createSiteResourceSchema = z
|
|||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (
|
if (data.mode === "host") {
|
||||||
data.mode === "host" ||
|
|
||||||
data.mode == "http"
|
|
||||||
) {
|
|
||||||
if (data.mode == "host") {
|
|
||||||
// Check if it's a valid IP address using zod (v4 or v6)
|
// Check if it's a valid IP address using zod (v4 or v6)
|
||||||
const isValidIP = z
|
const isValidIP = z
|
||||||
// .union([z.ipv4(), z.ipv6()])
|
// .union([z.ipv4(), z.ipv6()])
|
||||||
@@ -77,7 +72,6 @@ const createSiteResourceSchema = z
|
|||||||
if (isValidIP) {
|
if (isValidIP) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||||
const domainRegex =
|
const domainRegex =
|
||||||
@@ -111,21 +105,6 @@ const createSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.mode !== "http") return true;
|
|
||||||
return (
|
|
||||||
data.scheme !== undefined &&
|
|
||||||
data.destinationPort !== undefined &&
|
|
||||||
data.destinationPort >= 1 &&
|
|
||||||
data.destinationPort <= 65535
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"HTTP mode requires scheme (http or https) and a valid destination port"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
||||||
@@ -182,12 +161,11 @@ export async function createSiteResource(
|
|||||||
name,
|
name,
|
||||||
siteId,
|
siteId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
// protocol,
|
||||||
// proxyPort,
|
// proxyPort,
|
||||||
destinationPort,
|
// destinationPort,
|
||||||
destination,
|
destination,
|
||||||
enabled,
|
enabled,
|
||||||
ssl,
|
|
||||||
alias,
|
alias,
|
||||||
userIds,
|
userIds,
|
||||||
roleIds,
|
roleIds,
|
||||||
@@ -248,6 +226,30 @@ export async function createSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // check if resource with same protocol and proxy port already exists (only for port mode)
|
||||||
|
// if (mode === "port" && protocol && proxyPort) {
|
||||||
|
// const [existingResource] = await db
|
||||||
|
// .select()
|
||||||
|
// .from(siteResources)
|
||||||
|
// .where(
|
||||||
|
// and(
|
||||||
|
// eq(siteResources.siteId, siteId),
|
||||||
|
// eq(siteResources.orgId, orgId),
|
||||||
|
// eq(siteResources.protocol, protocol),
|
||||||
|
// eq(siteResources.proxyPort, proxyPort)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .limit(1);
|
||||||
|
// if (existingResource && existingResource.siteResourceId) {
|
||||||
|
// return next(
|
||||||
|
// createHttpError(
|
||||||
|
// HttpCode.CONFLICT,
|
||||||
|
// "A resource with the same protocol and proxy port already exists"
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// make sure the alias is unique within the org if provided
|
// make sure the alias is unique within the org if provided
|
||||||
if (alias) {
|
if (alias) {
|
||||||
const [conflict] = await db
|
const [conflict] = await db
|
||||||
@@ -278,7 +280,8 @@ export async function createSiteResource(
|
|||||||
|
|
||||||
const niceId = await getUniqueSiteResourceName(orgId);
|
const niceId = await getUniqueSiteResourceName(orgId);
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
if (mode === "host" || mode === "http") {
|
if (mode == "host") {
|
||||||
|
// we can only have an alias on a host
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +293,8 @@ export async function createSiteResource(
|
|||||||
niceId,
|
niceId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
mode,
|
mode: mode as "host" | "cidr",
|
||||||
ssl,
|
|
||||||
destination,
|
destination,
|
||||||
scheme,
|
|
||||||
destinationPort,
|
|
||||||
enabled,
|
enabled,
|
||||||
alias,
|
alias,
|
||||||
aliasAddress,
|
aliasAddress,
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
|||||||
}),
|
}),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
mode: z
|
mode: z
|
||||||
.enum(["host", "cidr", "http"])
|
.enum(["host", "cidr"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["host", "cidr", "http"],
|
enum: ["host", "cidr"],
|
||||||
description: "Filter site resources by mode"
|
description: "Filter site resources by mode"
|
||||||
}),
|
}),
|
||||||
sort_by: z
|
sort_by: z
|
||||||
@@ -88,8 +88,7 @@ function querySiteResourcesBase() {
|
|||||||
niceId: siteResources.niceId,
|
niceId: siteResources.niceId,
|
||||||
name: siteResources.name,
|
name: siteResources.name,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
ssl: siteResources.ssl,
|
protocol: siteResources.protocol,
|
||||||
scheme: siteResources.scheme,
|
|
||||||
proxyPort: siteResources.proxyPort,
|
proxyPort: siteResources.proxyPort,
|
||||||
destinationPort: siteResources.destinationPort,
|
destinationPort: siteResources.destinationPort,
|
||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
@@ -194,9 +193,7 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
||||||
|
|
||||||
const countQuery = db.$count(
|
const countQuery = db.$count(
|
||||||
querySiteResourcesBase()
|
querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources")
|
||||||
.where(and(...conditions))
|
|
||||||
.as("filtered_site_resources")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [siteResourcesList, totalCount] = await Promise.all([
|
const [siteResourcesList, totalCount] = await Promise.all([
|
||||||
|
|||||||
@@ -51,11 +51,10 @@ const updateSiteResourceSchema = z
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
||||||
mode: z.enum(["host", "cidr", "http"]).optional(),
|
mode: z.enum(["host", "cidr"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
scheme: z.enum(["http", "https"]).nullish(),
|
|
||||||
// proxyPort: z.int().positive().nullish(),
|
// proxyPort: z.int().positive().nullish(),
|
||||||
destinationPort: z.int().positive().nullish(),
|
// destinationPort: z.int().positive().nullish(),
|
||||||
destination: z.string().min(1).optional(),
|
destination: z.string().min(1).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
alias: z
|
alias: z
|
||||||
@@ -77,12 +76,7 @@ const updateSiteResourceSchema = z
|
|||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (
|
if (data.mode === "host" && data.destination) {
|
||||||
(data.mode === "host" ||
|
|
||||||
data.mode == "http") &&
|
|
||||||
data.destination
|
|
||||||
) {
|
|
||||||
if (data.mode == "host") {
|
|
||||||
const isValidIP = z
|
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
|
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||||
@@ -91,7 +85,6 @@ const updateSiteResourceSchema = z
|
|||||||
if (isValidIP) {
|
if (isValidIP) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||||
const domainRegex =
|
const domainRegex =
|
||||||
@@ -125,23 +118,6 @@ const updateSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.mode !== "http") return true;
|
|
||||||
return (
|
|
||||||
data.scheme !== undefined &&
|
|
||||||
data.scheme !== null &&
|
|
||||||
data.destinationPort !== undefined &&
|
|
||||||
data.destinationPort !== null &&
|
|
||||||
data.destinationPort >= 1 &&
|
|
||||||
data.destinationPort <= 65535
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"HTTP mode requires scheme (http or https) and a valid destination port"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
||||||
@@ -199,11 +175,8 @@ export async function updateSiteResource(
|
|||||||
siteId, // because it can change
|
siteId, // because it can change
|
||||||
niceId,
|
niceId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
|
||||||
destination,
|
destination,
|
||||||
destinationPort,
|
|
||||||
alias,
|
alias,
|
||||||
ssl,
|
|
||||||
enabled,
|
enabled,
|
||||||
userIds,
|
userIds,
|
||||||
roleIds,
|
roleIds,
|
||||||
@@ -373,10 +346,7 @@ export async function updateSiteResource(
|
|||||||
siteId,
|
siteId,
|
||||||
niceId,
|
niceId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
|
||||||
ssl,
|
|
||||||
destination,
|
destination,
|
||||||
destinationPort,
|
|
||||||
enabled,
|
enabled,
|
||||||
alias: alias && alias.trim() ? alias : null,
|
alias: alias && alias.trim() ? alias : null,
|
||||||
tcpPortRangeString,
|
tcpPortRangeString,
|
||||||
@@ -479,10 +449,7 @@ export async function updateSiteResource(
|
|||||||
name: name,
|
name: name,
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
scheme,
|
|
||||||
ssl,
|
|
||||||
destination: destination,
|
destination: destination,
|
||||||
destinationPort: destinationPort,
|
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
alias: alias && alias.trim() ? alias : null,
|
alias: alias && alias.trim() ? alias : null,
|
||||||
tcpPortRangeString: tcpPortRangeString,
|
tcpPortRangeString: tcpPortRangeString,
|
||||||
@@ -651,11 +618,11 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
|
|
||||||
// Only update targets on newt if destination changed
|
// Only update targets on newt if destination changed
|
||||||
if (destinationChanged || portRangesChanged) {
|
if (destinationChanged || portRangesChanged) {
|
||||||
const oldTarget = await generateSubnetProxyTargetV2(
|
const oldTarget = generateSubnetProxyTargetV2(
|
||||||
existingSiteResource,
|
existingSiteResource,
|
||||||
mergedAllClients
|
mergedAllClients
|
||||||
);
|
);
|
||||||
const newTarget = await generateSubnetProxyTargetV2(
|
const newTarget = generateSubnetProxyTargetV2(
|
||||||
updatedSiteResource,
|
updatedSiteResource,
|
||||||
mergedAllClients
|
mergedAllClients
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -235,9 +235,7 @@ export default async function migration() {
|
|||||||
for (const row of existingUserInviteRoles) {
|
for (const row of existingUserInviteRoles) {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO "userInviteRoles" ("inviteId", "roleId")
|
INSERT INTO "userInviteRoles" ("inviteId", "roleId")
|
||||||
SELECT ${row.inviteId}, ${row.roleId}
|
VALUES (${row.inviteId}, ${row.roleId})
|
||||||
WHERE EXISTS (SELECT 1 FROM "userInvites" WHERE "inviteId" = ${row.inviteId})
|
|
||||||
AND EXISTS (SELECT 1 FROM "roles" WHERE "roleId" = ${row.roleId})
|
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -260,10 +258,7 @@ export default async function migration() {
|
|||||||
for (const row of existingUserOrgRoles) {
|
for (const row of existingUserOrgRoles) {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO "userOrgRoles" ("userId", "orgId", "roleId")
|
INSERT INTO "userOrgRoles" ("userId", "orgId", "roleId")
|
||||||
SELECT ${row.userId}, ${row.orgId}, ${row.roleId}
|
VALUES (${row.userId}, ${row.orgId}, ${row.roleId})
|
||||||
WHERE EXISTS (SELECT 1 FROM "user" WHERE "id" = ${row.userId})
|
|
||||||
AND EXISTS (SELECT 1 FROM "orgs" WHERE "orgId" = ${row.orgId})
|
|
||||||
AND EXISTS (SELECT 1 FROM "roles" WHERE "roleId" = ${row.roleId})
|
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default async function migration() {
|
|||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO '__new_userOrgs'("userId", "orgId", "isOwner", "autoProvisioned", "pamUsername") SELECT "userId", "orgId", "isOwner", "autoProvisioned", "pamUsername" FROM 'userOrgs' WHERE EXISTS (SELECT 1 FROM 'user' WHERE id = userOrgs.userId) AND EXISTS (SELECT 1 FROM 'orgs' WHERE orgId = userOrgs.orgId);`
|
`INSERT INTO '__new_userOrgs'("userId", "orgId", "isOwner", "autoProvisioned", "pamUsername") SELECT "userId", "orgId", "isOwner", "autoProvisioned", "pamUsername" FROM 'userOrgs';`
|
||||||
).run();
|
).run();
|
||||||
db.prepare(`DROP TABLE 'userOrgs';`).run();
|
db.prepare(`DROP TABLE 'userOrgs';`).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
@@ -246,15 +246,12 @@ export default async function migration() {
|
|||||||
// Re-insert the preserved invite role assignments into the new userInviteRoles table
|
// Re-insert the preserved invite role assignments into the new userInviteRoles table
|
||||||
if (existingUserInviteRoles.length > 0) {
|
if (existingUserInviteRoles.length > 0) {
|
||||||
const insertUserInviteRole = db.prepare(
|
const insertUserInviteRole = db.prepare(
|
||||||
`INSERT OR IGNORE INTO 'userInviteRoles' ("inviteId", "roleId")
|
`INSERT OR IGNORE INTO 'userInviteRoles' ("inviteId", "roleId") VALUES (?, ?)`
|
||||||
SELECT ?, ?
|
|
||||||
WHERE EXISTS (SELECT 1 FROM 'userInvites' WHERE inviteId = ?)
|
|
||||||
AND EXISTS (SELECT 1 FROM 'roles' WHERE roleId = ?)`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertAll = db.transaction(() => {
|
const insertAll = db.transaction(() => {
|
||||||
for (const row of existingUserInviteRoles) {
|
for (const row of existingUserInviteRoles) {
|
||||||
insertUserInviteRole.run(row.inviteId, row.roleId, row.inviteId, row.roleId);
|
insertUserInviteRole.run(row.inviteId, row.roleId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,16 +265,12 @@ export default async function migration() {
|
|||||||
// Re-insert the preserved role assignments into the new userOrgRoles table
|
// Re-insert the preserved role assignments into the new userOrgRoles table
|
||||||
if (existingUserOrgRoles.length > 0) {
|
if (existingUserOrgRoles.length > 0) {
|
||||||
const insertUserOrgRole = db.prepare(
|
const insertUserOrgRole = db.prepare(
|
||||||
`INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId")
|
`INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId") VALUES (?, ?, ?)`
|
||||||
SELECT ?, ?, ?
|
|
||||||
WHERE EXISTS (SELECT 1 FROM 'user' WHERE id = ?)
|
|
||||||
AND EXISTS (SELECT 1 FROM 'orgs' WHERE orgId = ?)
|
|
||||||
AND EXISTS (SELECT 1 FROM 'roles' WHERE roleId = ?)`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertAll = db.transaction(() => {
|
const insertAll = db.transaction(() => {
|
||||||
for (const row of existingUserOrgRoles) {
|
for (const row of existingUserOrgRoles) {
|
||||||
insertUserOrgRole.run(row.userId, row.orgId, row.roleId, row.userId, row.orgId, row.roleId);
|
insertUserOrgRole.run(row.userId, row.orgId, row.roleId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,30 +56,18 @@ export default async function ClientResourcesPage(
|
|||||||
|
|
||||||
const internalResourceRows: InternalResourceRow[] = siteResources.map(
|
const internalResourceRows: InternalResourceRow[] = siteResources.map(
|
||||||
(siteResource) => {
|
(siteResource) => {
|
||||||
const rawMode = siteResource.mode as string | undefined;
|
|
||||||
const normalizedMode =
|
|
||||||
rawMode === "https"
|
|
||||||
? ("http" as const)
|
|
||||||
: rawMode === "host" || rawMode === "cidr" || rawMode === "http"
|
|
||||||
? rawMode
|
|
||||||
: ("host" as const);
|
|
||||||
return {
|
return {
|
||||||
id: siteResource.siteResourceId,
|
id: siteResource.siteResourceId,
|
||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
siteName: siteResource.siteName,
|
siteName: siteResource.siteName,
|
||||||
siteAddress: siteResource.siteAddress || null,
|
siteAddress: siteResource.siteAddress || null,
|
||||||
mode: normalizedMode,
|
mode: siteResource.mode || ("port" as any),
|
||||||
scheme:
|
|
||||||
siteResource.scheme ??
|
|
||||||
(rawMode === "https" ? ("https" as const) : null),
|
|
||||||
ssl:
|
|
||||||
siteResource.ssl === true || rawMode === "https",
|
|
||||||
// protocol: siteResource.protocol,
|
// protocol: siteResource.protocol,
|
||||||
// proxyPort: siteResource.proxyPort,
|
// proxyPort: siteResource.proxyPort,
|
||||||
siteId: siteResource.siteId,
|
siteId: siteResource.siteId,
|
||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
httpHttpsPort: siteResource.destinationPort ?? null,
|
// destinationPort: siteResource.destinationPort,
|
||||||
alias: siteResource.alias || null,
|
alias: siteResource.alias || null,
|
||||||
aliasAddress: siteResource.aliasAddress || null,
|
aliasAddress: siteResource.aliasAddress || null,
|
||||||
siteNiceId: siteResource.siteNiceId,
|
siteNiceId: siteResource.siteNiceId,
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Metadata } from "next";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@app/components/ui/card";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Private Placeholder"
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function MaintenanceScreen() {
|
|
||||||
const t = await getTranslations();
|
|
||||||
|
|
||||||
let title = t("privateMaintenanceScreenTitle");
|
|
||||||
let message = t("privateMaintenanceScreenMessage");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center p-4">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">{message}</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -46,15 +46,13 @@ export type InternalResourceRow = {
|
|||||||
siteName: string;
|
siteName: string;
|
||||||
siteAddress: string | null;
|
siteAddress: string | null;
|
||||||
// mode: "host" | "cidr" | "port";
|
// mode: "host" | "cidr" | "port";
|
||||||
mode: "host" | "cidr" | "http";
|
mode: "host" | "cidr";
|
||||||
scheme: "http" | "https" | null;
|
|
||||||
ssl: boolean;
|
|
||||||
// protocol: string | null;
|
// protocol: string | null;
|
||||||
// proxyPort: number | null;
|
// proxyPort: number | null;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteNiceId: string;
|
siteNiceId: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
httpHttpsPort: number | null;
|
// destinationPort: number | null;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
niceId: string;
|
niceId: string;
|
||||||
@@ -65,39 +63,6 @@ export type InternalResourceRow = {
|
|||||||
authDaemonPort?: number | null;
|
authDaemonPort?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveHttpHttpsDisplayPort(
|
|
||||||
mode: "http",
|
|
||||||
httpHttpsPort: number | null
|
|
||||||
): number {
|
|
||||||
if (httpHttpsPort != null) {
|
|
||||||
return httpHttpsPort;
|
|
||||||
}
|
|
||||||
return 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDestinationDisplay(row: InternalResourceRow): string {
|
|
||||||
const { mode, destination, httpHttpsPort, scheme } = row;
|
|
||||||
if (mode !== "http") {
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
const port = resolveHttpHttpsDisplayPort(mode, httpHttpsPort);
|
|
||||||
const downstreamScheme = scheme ?? "http";
|
|
||||||
const hostPart =
|
|
||||||
destination.includes(":") && !destination.startsWith("[")
|
|
||||||
? `[${destination}]`
|
|
||||||
: destination;
|
|
||||||
return `${downstreamScheme}://${hostPart}:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSafeUrlForLink(href: string): boolean {
|
|
||||||
try {
|
|
||||||
void new URL(href);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientResourcesTableProps = {
|
type ClientResourcesTableProps = {
|
||||||
internalResources: InternalResourceRow[];
|
internalResources: InternalResourceRow[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -250,10 +215,6 @@ export default function ClientResourcesTable({
|
|||||||
{
|
{
|
||||||
value: "cidr",
|
value: "cidr",
|
||||||
label: t("editInternalResourceDialogModeCidr")
|
label: t("editInternalResourceDialogModeCidr")
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "http",
|
|
||||||
label: t("editInternalResourceDialogModeHttp")
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
selectedValue={searchParams.get("mode") ?? undefined}
|
selectedValue={searchParams.get("mode") ?? undefined}
|
||||||
@@ -266,14 +227,10 @@ export default function ClientResourcesTable({
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
const modeLabels: Record<
|
const modeLabels: Record<"host" | "cidr" | "port", string> = {
|
||||||
"host" | "cidr" | "port" | "http",
|
|
||||||
string
|
|
||||||
> = {
|
|
||||||
host: t("editInternalResourceDialogModeHost"),
|
host: t("editInternalResourceDialogModeHost"),
|
||||||
cidr: t("editInternalResourceDialogModeCidr"),
|
cidr: t("editInternalResourceDialogModeCidr"),
|
||||||
port: t("editInternalResourceDialogModePort"),
|
port: t("editInternalResourceDialogModePort")
|
||||||
http: t("editInternalResourceDialogModeHttp")
|
|
||||||
};
|
};
|
||||||
return <span>{modeLabels[resourceRow.mode]}</span>;
|
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||||
}
|
}
|
||||||
@@ -286,12 +243,11 @@ export default function ClientResourcesTable({
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
const display = formatDestinationDisplay(resourceRow);
|
|
||||||
return (
|
return (
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={display}
|
text={resourceRow.destination}
|
||||||
isLink={false}
|
isLink={false}
|
||||||
displayText={display}
|
displayText={resourceRow.destination}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -304,27 +260,16 @@ export default function ClientResourcesTable({
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
if (resourceRow.mode === "host" && resourceRow.alias) {
|
return resourceRow.mode === "host" && resourceRow.alias ? (
|
||||||
return (
|
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={resourceRow.alias}
|
text={resourceRow.alias}
|
||||||
isLink={false}
|
isLink={false}
|
||||||
displayText={resourceRow.alias}
|
displayText={resourceRow.alias}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (resourceRow.mode === "http" && resourceRow.alias) {
|
|
||||||
const url = `${resourceRow.ssl ? "https" : "http"}://${resourceRow.alias}`;
|
|
||||||
return (
|
|
||||||
<CopyToClipboard
|
|
||||||
text={url}
|
|
||||||
isLink={isSafeUrlForLink(url)}
|
|
||||||
displayText={url}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span>-</span>;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "aliasAddress",
|
accessorKey: "aliasAddress",
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let data = { ...values };
|
let data = { ...values };
|
||||||
if (
|
if (data.mode === "host" && isHostname(data.destination)) {
|
||||||
(data.mode === "host" || data.mode === "http") &&
|
|
||||||
isHostname(data.destination)
|
|
||||||
) {
|
|
||||||
const currentAlias = data.alias?.trim() || "";
|
const currentAlias = data.alias?.trim() || "";
|
||||||
if (!currentAlias) {
|
if (!currentAlias) {
|
||||||
let aliasValue = data.destination;
|
let aliasValue = data.destination;
|
||||||
@@ -72,42 +69,21 @@ export default function CreateInternalResourceDialog({
|
|||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
...(data.mode === "http" && {
|
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined,
|
||||||
scheme: data.scheme,
|
|
||||||
ssl: data.ssl ?? false,
|
|
||||||
destinationPort: data.httpHttpsPort ?? undefined
|
|
||||||
}),
|
|
||||||
alias:
|
|
||||||
data.alias &&
|
|
||||||
typeof data.alias === "string" &&
|
|
||||||
data.alias.trim()
|
|
||||||
? data.alias
|
|
||||||
: undefined,
|
|
||||||
tcpPortRangeString: data.tcpPortRangeString,
|
tcpPortRangeString: data.tcpPortRangeString,
|
||||||
udpPortRangeString: data.udpPortRangeString,
|
udpPortRangeString: data.udpPortRangeString,
|
||||||
disableIcmp: data.disableIcmp ?? false,
|
disableIcmp: data.disableIcmp ?? false,
|
||||||
...(data.authDaemonMode != null && {
|
...(data.authDaemonMode != null && { authDaemonMode: data.authDaemonMode }),
|
||||||
authDaemonMode: data.authDaemonMode
|
...(data.authDaemonMode === "remote" && data.authDaemonPort != null && { authDaemonPort: data.authDaemonPort }),
|
||||||
}),
|
roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [],
|
||||||
...(data.authDaemonMode === "remote" &&
|
|
||||||
data.authDaemonPort != null && {
|
|
||||||
authDaemonPort: data.authDaemonPort
|
|
||||||
}),
|
|
||||||
roleIds: data.roles
|
|
||||||
? data.roles.map((r) => parseInt(r.id))
|
|
||||||
: [],
|
|
||||||
userIds: data.users ? data.users.map((u) => u.id) : [],
|
userIds: data.users ? data.users.map((u) => u.id) : [],
|
||||||
clientIds: data.clients
|
clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : []
|
||||||
? data.clients.map((c) => parseInt(c.id))
|
|
||||||
: []
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("createInternalResourceDialogSuccess"),
|
title: t("createInternalResourceDialogSuccess"),
|
||||||
description: t(
|
description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"),
|
||||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully"
|
|
||||||
),
|
|
||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -117,9 +93,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
title: t("createInternalResourceDialogError"),
|
title: t("createInternalResourceDialogError"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
error,
|
error,
|
||||||
t(
|
t("createInternalResourceDialogFailedToCreateInternalResource")
|
||||||
"createInternalResourceDialogFailedToCreateInternalResource"
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
@@ -132,13 +106,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
<Credenza open={open} onOpenChange={setOpen}>
|
<Credenza open={open} onOpenChange={setOpen}>
|
||||||
<CredenzaContent className="max-w-3xl">
|
<CredenzaContent className="max-w-3xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>{t("createInternalResourceDialogCreateClientResource")}</CredenzaTitle>
|
||||||
{t("createInternalResourceDialogCreateClientResource")}
|
|
||||||
</CredenzaTitle>
|
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
{t(
|
{t("createInternalResourceDialogCreateClientResourceDescription")}
|
||||||
"createInternalResourceDialogCreateClientResourceDescription"
|
|
||||||
)}
|
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
@@ -153,11 +123,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button
|
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{t("createInternalResourceDialogCancel")}
|
{t("createInternalResourceDialogCancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
|
|||||||
@@ -163,18 +163,15 @@ export default function DomainPicker({
|
|||||||
domainId: firstOrExistingDomain.domainId
|
domainId: firstOrExistingDomain.domainId
|
||||||
};
|
};
|
||||||
|
|
||||||
const base = firstOrExistingDomain.baseDomain;
|
|
||||||
const sub =
|
|
||||||
firstOrExistingDomain.type !== "cname"
|
|
||||||
? defaultSubdomain?.trim() || undefined
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
onDomainChange?.({
|
onDomainChange?.({
|
||||||
domainId: firstOrExistingDomain.domainId,
|
domainId: firstOrExistingDomain.domainId,
|
||||||
type: "organization",
|
type: "organization",
|
||||||
subdomain: sub,
|
subdomain:
|
||||||
fullDomain: sub ? `${sub}.${base}` : base,
|
firstOrExistingDomain.type !== "cname"
|
||||||
baseDomain: base
|
? defaultSubdomain || undefined
|
||||||
|
: undefined,
|
||||||
|
fullDomain: firstOrExistingDomain.baseDomain,
|
||||||
|
baseDomain: firstOrExistingDomain.baseDomain
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,9 +509,7 @@ export default function DomainPicker({
|
|||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{selectedBaseDomain.domain}
|
{selectedBaseDomain.domain}
|
||||||
</span>
|
</span>
|
||||||
{selectedBaseDomain.verified &&
|
{selectedBaseDomain.verified && (
|
||||||
selectedBaseDomain.domainType !==
|
|
||||||
"wildcard" && (
|
|
||||||
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -579,13 +574,6 @@ export default function DomainPicker({
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{orgDomain.type ===
|
|
||||||
"wildcard"
|
|
||||||
? t(
|
|
||||||
"domainPickerManual"
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<>
|
|
||||||
{orgDomain.type.toUpperCase()}{" "}
|
{orgDomain.type.toUpperCase()}{" "}
|
||||||
•{" "}
|
•{" "}
|
||||||
{orgDomain.verified
|
{orgDomain.verified
|
||||||
@@ -595,8 +583,6 @@ export default function DomainPicker({
|
|||||||
: t(
|
: t(
|
||||||
"domainPickerUnverified"
|
"domainPickerUnverified"
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Check
|
<Check
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ export default function EditInternalResourceDialog({
|
|||||||
async function handleSubmit(values: InternalResourceFormValues) {
|
async function handleSubmit(values: InternalResourceFormValues) {
|
||||||
try {
|
try {
|
||||||
let data = { ...values };
|
let data = { ...values };
|
||||||
if (
|
if (data.mode === "host" && isHostname(data.destination)) {
|
||||||
(data.mode === "host" || data.mode === "http") &&
|
|
||||||
isHostname(data.destination)
|
|
||||||
) {
|
|
||||||
const currentAlias = data.alias?.trim() || "";
|
const currentAlias = data.alias?.trim() || "";
|
||||||
if (!currentAlias) {
|
if (!currentAlias) {
|
||||||
let aliasValue = data.destination;
|
let aliasValue = data.destination;
|
||||||
@@ -74,11 +71,6 @@ export default function EditInternalResourceDialog({
|
|||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
...(data.mode === "http" && {
|
|
||||||
scheme: data.scheme,
|
|
||||||
ssl: data.ssl ?? false,
|
|
||||||
destinationPort: data.httpHttpsPort ?? null
|
|
||||||
}),
|
|
||||||
alias:
|
alias:
|
||||||
data.alias &&
|
data.alias &&
|
||||||
typeof data.alias === "string" &&
|
typeof data.alias === "string" &&
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
import {
|
|
||||||
OptionSelect,
|
|
||||||
type OptionSelectOption
|
|
||||||
} from "@app/components/OptionSelect";
|
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
@@ -49,8 +45,6 @@ import { z } from "zod";
|
|||||||
import { SitesSelector, type Selectedsite } from "./site-selector";
|
import { SitesSelector, type Selectedsite } from "./site-selector";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
|
||||||
|
|
||||||
// --- Helpers (shared) ---
|
// --- Helpers (shared) ---
|
||||||
|
|
||||||
@@ -126,14 +120,12 @@ export const cleanForFQDN = (name: string): string =>
|
|||||||
|
|
||||||
type Site = ListSitesResponse["sites"][0];
|
type Site = ListSitesResponse["sites"][0];
|
||||||
|
|
||||||
export type InternalResourceMode = "host" | "cidr" | "http";
|
|
||||||
|
|
||||||
export type InternalResourceData = {
|
export type InternalResourceData = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
mode: InternalResourceMode;
|
mode: "host" | "cidr";
|
||||||
siteId: number;
|
siteId: number;
|
||||||
niceId: string;
|
niceId: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
@@ -143,12 +135,6 @@ export type InternalResourceData = {
|
|||||||
disableIcmp?: boolean;
|
disableIcmp?: boolean;
|
||||||
authDaemonMode?: "site" | "remote" | null;
|
authDaemonMode?: "site" | "remote" | null;
|
||||||
authDaemonPort?: number | null;
|
authDaemonPort?: number | null;
|
||||||
httpHttpsPort?: number | null;
|
|
||||||
scheme?: "http" | "https" | null;
|
|
||||||
ssl?: boolean;
|
|
||||||
httpConfigSubdomain?: string | null;
|
|
||||||
httpConfigDomainId?: string | null;
|
|
||||||
httpConfigFullDomain?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagSchema = z.object({ id: z.string(), text: z.string() });
|
const tagSchema = z.object({ id: z.string(), text: z.string() });
|
||||||
@@ -156,7 +142,7 @@ const tagSchema = z.object({ id: z.string(), text: z.string() });
|
|||||||
export type InternalResourceFormValues = {
|
export type InternalResourceFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
mode: InternalResourceMode;
|
mode: "host" | "cidr";
|
||||||
destination: string;
|
destination: string;
|
||||||
alias?: string | null;
|
alias?: string | null;
|
||||||
niceId?: string;
|
niceId?: string;
|
||||||
@@ -165,12 +151,6 @@ export type InternalResourceFormValues = {
|
|||||||
disableIcmp?: boolean;
|
disableIcmp?: boolean;
|
||||||
authDaemonMode?: "site" | "remote" | null;
|
authDaemonMode?: "site" | "remote" | null;
|
||||||
authDaemonPort?: number | null;
|
authDaemonPort?: number | null;
|
||||||
httpHttpsPort?: number | null;
|
|
||||||
scheme?: "http" | "https";
|
|
||||||
ssl?: boolean;
|
|
||||||
httpConfigSubdomain?: string | null;
|
|
||||||
httpConfigDomainId?: string | null;
|
|
||||||
httpConfigFullDomain?: string | null;
|
|
||||||
roles?: z.infer<typeof tagSchema>[];
|
roles?: z.infer<typeof tagSchema>[];
|
||||||
users?: z.infer<typeof tagSchema>[];
|
users?: z.infer<typeof tagSchema>[];
|
||||||
clients?: z.infer<typeof tagSchema>[];
|
clients?: z.infer<typeof tagSchema>[];
|
||||||
@@ -231,22 +211,6 @@ export function InternalResourceForm({
|
|||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogModeCidr"
|
? "createInternalResourceDialogModeCidr"
|
||||||
: "editInternalResourceDialogModeCidr";
|
: "editInternalResourceDialogModeCidr";
|
||||||
const modeHttpKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogModeHttp"
|
|
||||||
: "editInternalResourceDialogModeHttp";
|
|
||||||
const schemeLabelKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogScheme"
|
|
||||||
: "editInternalResourceDialogScheme";
|
|
||||||
const enableSslLabelKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogEnableSsl"
|
|
||||||
: "editInternalResourceDialogEnableSsl";
|
|
||||||
const enableSslDescriptionKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogEnableSslDescription"
|
|
||||||
: "editInternalResourceDialogEnableSslDescription";
|
|
||||||
const destinationLabelKey =
|
const destinationLabelKey =
|
||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogDestination"
|
? "createInternalResourceDialogDestination"
|
||||||
@@ -259,27 +223,14 @@ export function InternalResourceForm({
|
|||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogAlias"
|
? "createInternalResourceDialogAlias"
|
||||||
: "editInternalResourceDialogAlias";
|
: "editInternalResourceDialogAlias";
|
||||||
const httpHttpsPortLabelKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogModePort"
|
|
||||||
: "editInternalResourceDialogModePort";
|
|
||||||
const httpConfigurationTitleKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogHttpConfiguration"
|
|
||||||
: "editInternalResourceDialogHttpConfiguration";
|
|
||||||
const httpConfigurationDescriptionKey =
|
|
||||||
variant === "create"
|
|
||||||
? "createInternalResourceDialogHttpConfigurationDescription"
|
|
||||||
: "editInternalResourceDialogHttpConfigurationDescription";
|
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z.object({
|
||||||
.object({
|
|
||||||
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
|
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
|
||||||
siteId: z
|
siteId: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
|
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
|
||||||
mode: z.enum(["host", "cidr", "http"]),
|
mode: z.enum(["host", "cidr"]),
|
||||||
destination: z
|
destination: z
|
||||||
.string()
|
.string()
|
||||||
.min(
|
.min(
|
||||||
@@ -289,18 +240,6 @@ export function InternalResourceForm({
|
|||||||
: undefined
|
: undefined
|
||||||
),
|
),
|
||||||
alias: z.string().nullish(),
|
alias: z.string().nullish(),
|
||||||
httpHttpsPort: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(65535)
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
|
||||||
ssl: z.boolean().optional(),
|
|
||||||
httpConfigSubdomain: z.string().nullish(),
|
|
||||||
httpConfigDomainId: z.string().nullish(),
|
|
||||||
httpConfigFullDomain: z.string().nullish(),
|
|
||||||
niceId: z
|
niceId: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -322,27 +261,6 @@ export function InternalResourceForm({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
})
|
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
if (data.mode !== "http") return;
|
|
||||||
if (!data.scheme) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: t("internalResourceDownstreamSchemeRequired"),
|
|
||||||
path: ["scheme"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
data.httpHttpsPort == null ||
|
|
||||||
!Number.isFinite(data.httpHttpsPort) ||
|
|
||||||
data.httpHttpsPort < 1
|
|
||||||
) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: t("internalResourceHttpPortRequired"),
|
|
||||||
path: ["httpHttpsPort"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
@@ -476,12 +394,6 @@ export function InternalResourceForm({
|
|||||||
disableIcmp: resource.disableIcmp ?? false,
|
disableIcmp: resource.disableIcmp ?? false,
|
||||||
authDaemonMode: resource.authDaemonMode ?? "site",
|
authDaemonMode: resource.authDaemonMode ?? "site",
|
||||||
authDaemonPort: resource.authDaemonPort ?? null,
|
authDaemonPort: resource.authDaemonPort ?? null,
|
||||||
httpHttpsPort: resource.httpHttpsPort ?? null,
|
|
||||||
scheme: resource.scheme ?? "http",
|
|
||||||
ssl: resource.ssl ?? false,
|
|
||||||
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
|
||||||
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
|
||||||
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
|
||||||
niceId: resource.niceId,
|
niceId: resource.niceId,
|
||||||
roles: [],
|
roles: [],
|
||||||
users: [],
|
users: [],
|
||||||
@@ -493,12 +405,6 @@ export function InternalResourceForm({
|
|||||||
mode: "host",
|
mode: "host",
|
||||||
destination: "",
|
destination: "",
|
||||||
alias: null,
|
alias: null,
|
||||||
httpHttpsPort: null,
|
|
||||||
scheme: "http",
|
|
||||||
ssl: true,
|
|
||||||
httpConfigSubdomain: null,
|
|
||||||
httpConfigDomainId: null,
|
|
||||||
httpConfigFullDomain: null,
|
|
||||||
tcpPortRangeString: "*",
|
tcpPortRangeString: "*",
|
||||||
udpPortRangeString: "*",
|
udpPortRangeString: "*",
|
||||||
disableIcmp: false,
|
disableIcmp: false,
|
||||||
@@ -519,10 +425,6 @@ export function InternalResourceForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mode = form.watch("mode");
|
const mode = form.watch("mode");
|
||||||
const httpConfigSubdomain = form.watch("httpConfigSubdomain");
|
|
||||||
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
|
||||||
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
|
||||||
const isHttpMode = mode === "http";
|
|
||||||
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
const previousResourceId = useRef<number | null>(null);
|
const previousResourceId = useRef<number | null>(null);
|
||||||
@@ -546,12 +448,6 @@ export function InternalResourceForm({
|
|||||||
mode: "host",
|
mode: "host",
|
||||||
destination: "",
|
destination: "",
|
||||||
alias: null,
|
alias: null,
|
||||||
httpHttpsPort: null,
|
|
||||||
scheme: "http",
|
|
||||||
ssl: true,
|
|
||||||
httpConfigSubdomain: null,
|
|
||||||
httpConfigDomainId: null,
|
|
||||||
httpConfigFullDomain: null,
|
|
||||||
tcpPortRangeString: "*",
|
tcpPortRangeString: "*",
|
||||||
udpPortRangeString: "*",
|
udpPortRangeString: "*",
|
||||||
disableIcmp: false,
|
disableIcmp: false,
|
||||||
@@ -579,12 +475,6 @@ export function InternalResourceForm({
|
|||||||
mode: resource.mode ?? "host",
|
mode: resource.mode ?? "host",
|
||||||
destination: resource.destination ?? "",
|
destination: resource.destination ?? "",
|
||||||
alias: resource.alias ?? null,
|
alias: resource.alias ?? null,
|
||||||
httpHttpsPort: resource.httpHttpsPort ?? null,
|
|
||||||
scheme: resource.scheme ?? "http",
|
|
||||||
ssl: resource.ssl ?? false,
|
|
||||||
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
|
||||||
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
|
||||||
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
|
||||||
tcpPortRangeString: resource.tcpPortRangeString ?? "*",
|
tcpPortRangeString: resource.tcpPortRangeString ?? "*",
|
||||||
udpPortRangeString: resource.udpPortRangeString ?? "*",
|
udpPortRangeString: resource.udpPortRangeString ?? "*",
|
||||||
disableIcmp: resource.disableIcmp ?? false,
|
disableIcmp: resource.disableIcmp ?? false,
|
||||||
@@ -691,50 +581,12 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<HorizontalTabs
|
|
||||||
clientSide
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
title: t(
|
|
||||||
"editInternalResourceDialogNetworkSettings"
|
|
||||||
),
|
|
||||||
href: "#"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("editInternalResourceDialogAccessPolicy"),
|
|
||||||
href: "#"
|
|
||||||
},
|
|
||||||
...(disableEnterpriseFeatures || mode !== "host"
|
|
||||||
? []
|
|
||||||
: [{ title: t("sshAccess"), href: "#" }])
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div className="space-y-4 mt-4 p-1">
|
|
||||||
<div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<label className="font-medium block">
|
|
||||||
{t(
|
|
||||||
"editInternalResourceDialogDestinationLabel"
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"editInternalResourceDialogDestinationDescription"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-4 items-start mb-4">
|
|
||||||
<div className="min-w-0 col-span-1">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="siteId"
|
name="siteId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>
|
<FormLabel>{t("site")}</FormLabel>
|
||||||
{t("site")}
|
|
||||||
</FormLabel>
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -753,9 +605,7 @@ export function InternalResourceForm({
|
|||||||
s.siteId ===
|
s.siteId ===
|
||||||
field.value
|
field.value
|
||||||
)?.name
|
)?.name
|
||||||
: t(
|
: t("selectSite")}
|
||||||
"selectSite"
|
|
||||||
)}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -763,21 +613,11 @@ export function InternalResourceForm({
|
|||||||
<PopoverContent className="w-full p-0">
|
<PopoverContent className="w-full p-0">
|
||||||
<SitesSelector
|
<SitesSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
selectedSite={
|
selectedSite={selectedSite}
|
||||||
selectedSite
|
filterTypes={["newt"]}
|
||||||
}
|
onSelectSite={(site) => {
|
||||||
filterTypes={[
|
setSelectedSite(site);
|
||||||
"newt"
|
field.onChange(site.siteId);
|
||||||
]}
|
|
||||||
onSelectSite={(
|
|
||||||
site
|
|
||||||
) => {
|
|
||||||
setSelectedSite(
|
|
||||||
site
|
|
||||||
);
|
|
||||||
field.onChange(
|
|
||||||
site.siteId
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@@ -787,84 +627,79 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 col-span-2">
|
|
||||||
<FormField
|
<HorizontalTabs
|
||||||
control={form.control}
|
clientSide
|
||||||
name="mode"
|
items={[
|
||||||
render={({ field }) => {
|
|
||||||
const modeOptions: OptionSelectOption<InternalResourceMode>[] =
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
value: "host",
|
title: t(
|
||||||
label: t(modeHostKey)
|
"editInternalResourceDialogNetworkSettings"
|
||||||
|
),
|
||||||
|
href: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cidr",
|
title: t("editInternalResourceDialogAccessPolicy"),
|
||||||
label: t(modeCidrKey)
|
href: "#"
|
||||||
},
|
},
|
||||||
{
|
...(disableEnterpriseFeatures || mode === "cidr"
|
||||||
value: "http",
|
? []
|
||||||
label: t(modeHttpKey)
|
: [{ title: t("sshAccess"), href: "#" }])
|
||||||
}
|
]}
|
||||||
];
|
>
|
||||||
return (
|
<div className="space-y-4 mt-4 p-1">
|
||||||
<FormItem>
|
<div>
|
||||||
<FormLabel>
|
<div className="mb-8">
|
||||||
{t(modeLabelKey)}
|
<label className="font-medium block">
|
||||||
</FormLabel>
|
{t(
|
||||||
<OptionSelect<InternalResourceMode>
|
"editInternalResourceDialogDestinationLabel"
|
||||||
options={modeOptions}
|
)}
|
||||||
value={field.value}
|
</label>
|
||||||
onChange={
|
<div className="text-sm text-muted-foreground">
|
||||||
field.onChange
|
{t(
|
||||||
}
|
"editInternalResourceDialogDestinationDescription"
|
||||||
cols={3}
|
)}
|
||||||
/>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid gap-4 items-start",
|
"grid gap-4 items-start",
|
||||||
mode === "cidr" && "grid-cols-1",
|
mode === "cidr"
|
||||||
mode === "http" && "grid-cols-3",
|
? "grid-cols-4"
|
||||||
mode === "host" && "grid-cols-2"
|
: "grid-cols-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mode === "http" && (
|
<div
|
||||||
<div className="min-w-0">
|
className={
|
||||||
|
mode === "cidr"
|
||||||
|
? "col-span-1"
|
||||||
|
: "col-span-3"
|
||||||
|
}
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="scheme"
|
name="mode"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(schemeLabelKey)}
|
{t(modeLabelKey)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
value={
|
value={field.value}
|
||||||
field.value ??
|
|
||||||
"http"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">
|
<SelectItem value="host">
|
||||||
http
|
{t(modeHostKey)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="https">
|
<SelectItem value="cidr">
|
||||||
https
|
{t(modeCidrKey)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -873,13 +708,12 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={
|
||||||
mode === "cidr" && "col-span-1",
|
mode === "cidr"
|
||||||
(mode === "http" || mode === "host") &&
|
? "col-span-3"
|
||||||
"min-w-0"
|
: "col-span-5"
|
||||||
)}
|
}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -890,18 +724,15 @@ export function InternalResourceForm({
|
|||||||
{t(destinationLabelKey)}
|
{t(destinationLabelKey)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
{...field}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode === "host" && (
|
{mode !== "cidr" && (
|
||||||
<div className="min-w-0">
|
<div className="col-span-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="alias"
|
name="alias"
|
||||||
@@ -913,7 +744,6 @@ export function InternalResourceForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full"
|
|
||||||
value={
|
value={
|
||||||
field.value ??
|
field.value ??
|
||||||
""
|
""
|
||||||
@@ -926,144 +756,9 @@ export function InternalResourceForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mode === "http" && (
|
|
||||||
<div className="min-w-0">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="httpHttpsPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
httpHttpsPortLabelKey
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={65535}
|
|
||||||
value={
|
|
||||||
field.value ??
|
|
||||||
""
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const raw =
|
|
||||||
e.target
|
|
||||||
.value;
|
|
||||||
if (
|
|
||||||
raw === ""
|
|
||||||
) {
|
|
||||||
field.onChange(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const n =
|
|
||||||
Number(raw);
|
|
||||||
field.onChange(
|
|
||||||
Number.isFinite(
|
|
||||||
n
|
|
||||||
)
|
|
||||||
? n
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isHttpMode ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="my-8">
|
|
||||||
<label className="font-medium block">
|
|
||||||
{t(httpConfigurationTitleKey)}
|
|
||||||
</label>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t(httpConfigurationDescriptionKey)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DomainPicker
|
|
||||||
key={
|
|
||||||
variant === "edit" && siteResourceId
|
|
||||||
? `http-domain-${siteResourceId}`
|
|
||||||
: "http-domain-create"
|
|
||||||
}
|
|
||||||
orgId={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
|
|
||||||
);
|
|
||||||
form.setValue("alias", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.setValue(
|
|
||||||
"httpConfigSubdomain",
|
|
||||||
res.subdomain ?? null
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"httpConfigDomainId",
|
|
||||||
res.domainId
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"httpConfigFullDomain",
|
|
||||||
res.fullDomain
|
|
||||||
);
|
|
||||||
form.setValue("alias", res.fullDomain);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="ssl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<SwitchInput
|
|
||||||
id="internal-resource-ssl"
|
|
||||||
label={t(enableSslLabelKey)}
|
|
||||||
description={t(
|
|
||||||
enableSslDescriptionKey
|
|
||||||
)}
|
|
||||||
checked={!!field.value}
|
|
||||||
onCheckedChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="my-8">
|
<div className="my-8">
|
||||||
<label className="font-medium block">
|
<label className="font-medium block">
|
||||||
@@ -1111,11 +806,7 @@ export function InternalResourceForm({
|
|||||||
value={tcpPortMode}
|
value={tcpPortMode}
|
||||||
onValueChange={(
|
onValueChange={(
|
||||||
v: PortMode
|
v: PortMode
|
||||||
) =>
|
) => setTcpPortMode(v)}
|
||||||
setTcpPortMode(
|
|
||||||
v
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
@@ -1124,19 +815,13 @@ export function InternalResourceForm({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t(
|
{t("allPorts")}
|
||||||
"allPorts"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t(
|
{t("blocked")}
|
||||||
"blocked"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t(
|
{t("custom")}
|
||||||
"custom"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1148,12 +833,9 @@ export function InternalResourceForm({
|
|||||||
value={
|
value={
|
||||||
tcpCustomPorts
|
tcpCustomPorts
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={(e) =>
|
||||||
e
|
|
||||||
) =>
|
|
||||||
setTcpCustomPorts(
|
setTcpCustomPorts(
|
||||||
e
|
e.target
|
||||||
.target
|
|
||||||
.value
|
.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1217,11 +899,7 @@ export function InternalResourceForm({
|
|||||||
value={udpPortMode}
|
value={udpPortMode}
|
||||||
onValueChange={(
|
onValueChange={(
|
||||||
v: PortMode
|
v: PortMode
|
||||||
) =>
|
) => setUdpPortMode(v)}
|
||||||
setUdpPortMode(
|
|
||||||
v
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
@@ -1230,19 +908,13 @@ export function InternalResourceForm({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t(
|
{t("allPorts")}
|
||||||
"allPorts"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t(
|
{t("blocked")}
|
||||||
"blocked"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t(
|
{t("custom")}
|
||||||
"custom"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1254,12 +926,9 @@ export function InternalResourceForm({
|
|||||||
value={
|
value={
|
||||||
udpCustomPorts
|
udpCustomPorts
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={(e) =>
|
||||||
e
|
|
||||||
) =>
|
|
||||||
setUdpCustomPorts(
|
setUdpCustomPorts(
|
||||||
e
|
e.target
|
||||||
.target
|
|
||||||
.value
|
.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1303,9 +972,7 @@ export function InternalResourceForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
{t(
|
{t("editInternalResourceDialogIcmp")}
|
||||||
"editInternalResourceDialogIcmp"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1348,7 +1015,6 @@ export function InternalResourceForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 mt-4 p-1">
|
<div className="space-y-4 mt-4 p-1">
|
||||||
@@ -1547,8 +1213,8 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SSH Access tab (host mode only) */}
|
{/* SSH Access tab */}
|
||||||
{!disableEnterpriseFeatures && mode === "host" && (
|
{!disableEnterpriseFeatures && mode !== "cidr" && (
|
||||||
<div className="space-y-4 mt-4 p-1">
|
<div className="space-y-4 mt-4 p-1">
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
Reference in New Issue
Block a user