mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-10 20:06:37 +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": "Безплатен предоставен домейн",
|
||||
"domainPickerVerified": "Проверено",
|
||||
"domainPickerUnverified": "Непроверено",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
|
||||
"domainPickerError": "Грешка",
|
||||
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména",
|
||||
"domainPickerVerified": "Ověřeno",
|
||||
"domainPickerUnverified": "Neověřeno",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.",
|
||||
"domainPickerError": "Chyba",
|
||||
"domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
||||
"domainPickerVerified": "Verifiziert",
|
||||
"domainPickerUnverified": "Nicht verifiziert",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
||||
"domainPickerError": "Fehler",
|
||||
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains",
|
||||
|
||||
@@ -1817,11 +1817,6 @@
|
||||
"editInternalResourceDialogModePort": "Port",
|
||||
"editInternalResourceDialogModeHost": "Host",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -1865,19 +1860,11 @@
|
||||
"createInternalResourceDialogModePort": "Port",
|
||||
"createInternalResourceDialogModeHost": "Host",
|
||||
"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",
|
||||
"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.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"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",
|
||||
"siteAcceptClientConnections": "Accept Client Connections",
|
||||
"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",
|
||||
"domainPickerVerified": "Verified",
|
||||
"domainPickerUnverified": "Unverified",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||
"domainPickerError": "Error",
|
||||
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||
@@ -2673,12 +2659,8 @@
|
||||
"editInternalResourceDialogAddUsers": "Add Users",
|
||||
"editInternalResourceDialogAddClients": "Add Clients",
|
||||
"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.",
|
||||
"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",
|
||||
"editInternalResourceDialogUdp": "UDP",
|
||||
"editInternalResourceDialogIcmp": "ICMP",
|
||||
@@ -2717,8 +2699,6 @@
|
||||
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
||||
"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",
|
||||
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
||||
"editDomain": "Edit Domain",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||
"domainPickerVerified": "Verificado",
|
||||
"domainPickerUnverified": "Sin verificar",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
||||
"domainPickerError": "Error",
|
||||
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||
"domainPickerVerified": "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.",
|
||||
"domainPickerError": "Erreur",
|
||||
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"setupNewOrg": "Nuova Organizzazione",
|
||||
"setupCreateOrg": "Crea Organizzazione",
|
||||
"setupCreateResources": "Crea Risorse",
|
||||
"setupOrgName": "Nome Dell'Organizzazione",
|
||||
"setupOrgName": "Nome dell'Organizzazione",
|
||||
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
||||
"orgId": "Id Organizzazione",
|
||||
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
||||
"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.",
|
||||
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
||||
"welcome": "Benvenuti a Pangolin",
|
||||
"welcomeTo": "Benvenuto a",
|
||||
"welcome": "Benvenuto su Pangolin!",
|
||||
"welcomeTo": "Benvenuto su Pangolin!",
|
||||
"componentsCreateOrg": "Crea un'organizzazione",
|
||||
"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à.",
|
||||
@@ -27,7 +27,7 @@
|
||||
"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.",
|
||||
"inviteCreateUser": "Si prega di creare un account prima.",
|
||||
"goHome": "Vai A Home",
|
||||
"goHome": "Vai alla Home",
|
||||
"inviteLogInOtherUser": "Accedi come utente diverso",
|
||||
"createAnAccount": "Crea un account",
|
||||
"inviteNotAccepted": "Invito Non Accettato",
|
||||
@@ -51,7 +51,7 @@
|
||||
"edit": "Modifica",
|
||||
"siteConfirmDelete": "Conferma Eliminazione 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?",
|
||||
"siteManageSites": "Gestisci Siti",
|
||||
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
||||
@@ -75,9 +75,9 @@
|
||||
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
||||
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
||||
"toggle": "Attiva/disattiva",
|
||||
"dockerCompose": "Composizione Docker",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"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",
|
||||
"searchSitesProgress": "Cerca siti...",
|
||||
"siteAdd": "Aggiungi Sito",
|
||||
@@ -88,29 +88,29 @@
|
||||
"operatingSystem": "Sistema Operativo",
|
||||
"commands": "Comandi",
|
||||
"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",
|
||||
"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",
|
||||
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
||||
"siteUpdated": "Sito aggiornato",
|
||||
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
||||
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
||||
"siteSettingDescription": "Configura le impostazioni del sito",
|
||||
"siteSetting": "Impostazioni {siteName}",
|
||||
"siteSetting": "Impostazioni del sito {siteName}",
|
||||
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
||||
"siteWg": "WireGuard Base",
|
||||
"siteWgDescription": "Usa 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",
|
||||
"siteWgDescription": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteWgDescriptionSaas": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
||||
"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",
|
||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server",
|
||||
"remoteNodeCredentialsDescription": "Questo è come il nodo remoto si autenticherà con il server",
|
||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticherà con il server",
|
||||
"remoteNodeCredentialsDescription": "Questo è il modo in cui il nodo remoto si autenticherà con il server",
|
||||
"siteCredentialsSave": "Salva le credenziali",
|
||||
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
||||
"siteInfo": "Informazioni Sito",
|
||||
@@ -140,8 +140,8 @@
|
||||
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
||||
"shareTitleOptional": "Titolo (facoltativo)",
|
||||
"expireIn": "Scadenza In",
|
||||
"neverExpire": "Mai scadere",
|
||||
"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.",
|
||||
"neverExpire": "Nessuna scadenza",
|
||||
"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.",
|
||||
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
||||
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
||||
@@ -161,9 +161,9 @@
|
||||
"never": "Mai",
|
||||
"shareErrorSelectResource": "Seleziona una risorsa",
|
||||
"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",
|
||||
"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",
|
||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||
@@ -174,12 +174,12 @@
|
||||
"authentication": "Autenticazione",
|
||||
"protected": "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?",
|
||||
"resourceHTTP": "Risorsa HTTPS",
|
||||
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
||||
"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.",
|
||||
"resourceCreate": "Crea Risorsa",
|
||||
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
||||
@@ -192,7 +192,7 @@
|
||||
"selectCountry": "Seleziona paese",
|
||||
"searchCountries": "Cerca paesi...",
|
||||
"noCountryFound": "Nessun paese trovato.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
||||
"resourceType": "Tipo Di Risorsa",
|
||||
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||
@@ -206,13 +206,13 @@
|
||||
"protocol": "Protocollo",
|
||||
"protocolSelect": "Seleziona un protocollo",
|
||||
"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",
|
||||
"cancel": "Annulla",
|
||||
"resourceConfig": "Snippet Di Configurazione",
|
||||
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
||||
"resourceAddEntrypoints": "Traefik: Aggiungi Ingresso",
|
||||
"resourceExposePorts": "Gerbil: espone le porte in Docker componi",
|
||||
"resourceAddEntrypoints": "Traefik: Aggiungi Entrypoint",
|
||||
"resourceExposePorts": "Gerbil: espone le porte in Docker Compose",
|
||||
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
||||
"resourceBack": "Torna alle risorse",
|
||||
"resourceGoTo": "Vai alla Risorsa",
|
||||
@@ -228,7 +228,7 @@
|
||||
"rules": "Regole",
|
||||
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
||||
"resourceSetting": "Impostazioni {resourceName}",
|
||||
"alwaysAllow": "Autenticazione Bypass",
|
||||
"alwaysAllow": "Bypass Autenticazione",
|
||||
"alwaysDeny": "Blocca Accesso",
|
||||
"passToAuth": "Passa all'autenticazione",
|
||||
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
||||
@@ -237,11 +237,11 @@
|
||||
"saveGeneralSettings": "Salva Impostazioni Generali",
|
||||
"saveSettings": "Salva Impostazioni",
|
||||
"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",
|
||||
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
||||
"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?",
|
||||
"orgUpdated": "Organizzazione aggiornata",
|
||||
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
||||
@@ -254,10 +254,10 @@
|
||||
"orgDeleted": "Organizzazione eliminata",
|
||||
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
||||
"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",
|
||||
"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",
|
||||
"deleteAccountSuccess": "Account Eliminato",
|
||||
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
||||
@@ -272,7 +272,7 @@
|
||||
"accessUserCreate": "Crea Utente",
|
||||
"accessUserRemove": "Rimuovi Utente",
|
||||
"username": "Nome utente",
|
||||
"identityProvider": "Provider Di Identità",
|
||||
"identityProvider": "Provider Identità",
|
||||
"role": "Ruolo",
|
||||
"nameRequired": "Il nome è obbligatorio",
|
||||
"accessRolesManage": "Gestisci Ruoli",
|
||||
@@ -328,8 +328,8 @@
|
||||
"apiKeysDelete": "Elimina Chiave API",
|
||||
"apiKeysManage": "Gestisci Chiavi API",
|
||||
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
||||
"provisioningKeysTitle": "Chiave Di Provvedimento",
|
||||
"provisioningKeysManage": "Gestisci Chiavi Di Provvedimento",
|
||||
"provisioningKeysTitle": "Chiave di provisioning",
|
||||
"provisioningKeysManage": "Gestisci Chiavi di provisioning",
|
||||
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
||||
"provisioningManage": "Accantonamento",
|
||||
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
||||
@@ -337,25 +337,25 @@
|
||||
"siteApproveSuccess": "Sito approvato con successo",
|
||||
"siteApproveError": "Errore nell'approvazione del sito",
|
||||
"provisioningKeys": "Chiavi Di Provvedimento",
|
||||
"searchProvisioningKeys": "Cerca i tasti di provisioning ...",
|
||||
"provisioningKeysAdd": "Genera Chiave Di Provvedimento",
|
||||
"provisioningKeysErrorDelete": "Errore nell'eliminare la chiave di provisioning",
|
||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminare la chiave di provisioning",
|
||||
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
|
||||
"provisioningKeysAdd": "Genera Chiave di provisioning",
|
||||
"provisioningKeysErrorDelete": "Errore nell'eliminazione della chiave di provisioning",
|
||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave di provisioning",
|
||||
"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.",
|
||||
"provisioningKeysDeleteConfirm": "Conferma Elimina Chiave Provvisoria",
|
||||
"provisioningKeysDeleteConfirm": "Conferma Eliminazione della 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",
|
||||
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
||||
"provisioningKeysSave": "Salva la chiave di provisioning",
|
||||
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
||||
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
||||
"provisioningKeysList": "Nuova chiave di provisioning",
|
||||
"provisioningKeysMaxBatchSize": "Dimensione massima lotto",
|
||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del lotto (nessun limite)",
|
||||
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
|
||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
|
||||
"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",
|
||||
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
||||
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
||||
@@ -363,14 +363,14 @@
|
||||
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
||||
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
||||
"provisioningKeysNeverUsed": "Mai",
|
||||
"provisioningKeysEdit": "Modifica Chiave Di Provvedimento",
|
||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del lotto e il tempo di scadenza per questa chiave.",
|
||||
"provisioningKeysEdit": "Modifica Chiave di provisioning",
|
||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
|
||||
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
||||
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
||||
"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.",
|
||||
"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.",
|
||||
"provisioningKeysBannerButtonText": "Scopri di più",
|
||||
"pendingSitesBannerTitle": "Siti In Attesa",
|
||||
@@ -386,7 +386,7 @@
|
||||
"userErrorDelete": "Errore nell'eliminare l'utente",
|
||||
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
||||
"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?",
|
||||
"licenseKey": "Chiave Di Licenza",
|
||||
"valid": "Valido",
|
||||
@@ -404,9 +404,9 @@
|
||||
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
||||
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
||||
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
||||
"licenseAbout": "Informazioni Su Licenze",
|
||||
"licenseAbout": "Informazioni sul Licensing",
|
||||
"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",
|
||||
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
||||
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
||||
@@ -429,7 +429,7 @@
|
||||
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
||||
"licensedNot": "Non Licenziato",
|
||||
"hostId": "ID Host",
|
||||
"licenseReckeckAll": "Ricontrolla Tutte Le Tasti",
|
||||
"licenseReckeckAll": "Ricontrolla Tutte le chiavi",
|
||||
"licenseSiteUsage": "Utilizzo Siti",
|
||||
"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.",
|
||||
@@ -480,7 +480,7 @@
|
||||
"userOrgRemoved": "Utente rimosso",
|
||||
"userOrgRemovedDescription": "L'utente {email} è stato rimosso 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",
|
||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||
"users": "Utenti",
|
||||
@@ -532,13 +532,13 @@
|
||||
"approve": "Approva",
|
||||
"approved": "Approvato",
|
||||
"denied": "Negato",
|
||||
"deniedApproval": "Omologazione Negata",
|
||||
"deniedApproval": "Approvazione Negata",
|
||||
"all": "Tutti",
|
||||
"deny": "Nega",
|
||||
"viewDetails": "Visualizza Dettagli",
|
||||
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
||||
"resetFilters": "Ripristina Filtri",
|
||||
"totalBlocked": "Richieste Bloccate Da Pangolino",
|
||||
"totalBlocked": "Richieste Bloccate Da Pangolin",
|
||||
"totalRequests": "Totale Richieste",
|
||||
"requestsByCountry": "Richieste Per Paese",
|
||||
"requestsByDay": "Richieste Per Giorno",
|
||||
@@ -546,7 +546,7 @@
|
||||
"allowed": "Consentito",
|
||||
"topCountries": "Paesi Principali",
|
||||
"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.",
|
||||
"inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
|
||||
"idpTitle": "Informazioni Generali",
|
||||
@@ -562,7 +562,7 @@
|
||||
"userSaved": "Utente salvato",
|
||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||
"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à",
|
||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||
"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.",
|
||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||
"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",
|
||||
"configureTarget": "Configura Obiettivi",
|
||||
"configureTarget": "Configura Risorse Interne",
|
||||
"targetErrorFetch": "Impossibile recuperare i target",
|
||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||
"domainPickerVerified": "Verificato",
|
||||
"domainPickerUnverified": "Non Verificato",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
||||
"domainPickerError": "Errore",
|
||||
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||
"domainPickerVerified": "검증됨",
|
||||
"domainPickerUnverified": "검증되지 않음",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
||||
"domainPickerError": "오류",
|
||||
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
||||
"domainPickerVerified": "Bekreftet",
|
||||
"domainPickerUnverified": "Uverifisert",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
||||
"domainPickerError": "Feil",
|
||||
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||
"domainPickerVerified": "Geverifieerd",
|
||||
"domainPickerUnverified": "Ongeverifieerd",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
||||
"domainPickerError": "Foutmelding",
|
||||
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||
"domainPickerVerified": "Zweryfikowano",
|
||||
"domainPickerUnverified": "Niezweryfikowane",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
||||
"domainPickerError": "Błąd",
|
||||
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||
"domainPickerVerified": "Verificada",
|
||||
"domainPickerUnverified": "Não verificado",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
||||
"domainPickerError": "ERRO",
|
||||
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||
"domainPickerVerified": "Подтверждено",
|
||||
"domainPickerUnverified": "Не подтверждено",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
||||
"domainPickerError": "Ошибка",
|
||||
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
||||
"domainPickerVerified": "Doğrulandı",
|
||||
"domainPickerUnverified": "Doğrulanmadı",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
||||
"domainPickerError": "Hata",
|
||||
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
||||
|
||||
@@ -2116,6 +2116,7 @@
|
||||
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||
"domainPickerVerified": "已验证",
|
||||
"domainPickerUnverified": "未验证",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
||||
"domainPickerError": "错误",
|
||||
"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
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer(
|
||||
"settingsLogRetentionDaysConnection"
|
||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
@@ -103,9 +101,7 @@ export const sites = pgTable("sites", {
|
||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||
status: varchar("status")
|
||||
.$type<"pending" | "approved">()
|
||||
.default("approved")
|
||||
status: varchar("status").$type<"pending" | "approved">().default("approved")
|
||||
});
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
@@ -234,9 +230,8 @@ export const siteResources = pgTable("siteResources", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: varchar("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
ssl: boolean("ssl").notNull().default(false),
|
||||
mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
||||
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
protocol: varchar("protocol"), // only for port mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port 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
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer(
|
||||
"settingsLogRetentionDaysConnection"
|
||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
@@ -260,9 +258,8 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: text("name").notNull(),
|
||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||
mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
||||
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||
mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
protocol: text("protocol"), // only for port mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||
|
||||
@@ -22,7 +22,6 @@ import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||
|
||||
async function startServers() {
|
||||
@@ -40,7 +39,6 @@ async function startServers() {
|
||||
initTelemetryClient();
|
||||
|
||||
initLogCleanupInterval();
|
||||
initAcmeCertSync();
|
||||
|
||||
// Start all servers
|
||||
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 { 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 = {
|
||||
newSiteResource: SiteResource;
|
||||
oldSiteResource?: SiteResource;
|
||||
@@ -90,18 +76,14 @@ export async function updateClientResources(
|
||||
}
|
||||
|
||||
if (existingResource) {
|
||||
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
||||
// Update existing resource
|
||||
const [updatedResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name: resourceData.name || resourceNiceId,
|
||||
siteId: site.siteId,
|
||||
mode: mappedMode.mode,
|
||||
ssl: mappedMode.ssl,
|
||||
scheme: mappedMode.scheme,
|
||||
mode: resourceData.mode,
|
||||
destination: resourceData.destination,
|
||||
destinationPort: resourceData["destination-port"],
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null,
|
||||
@@ -225,9 +207,9 @@ export async function updateClientResources(
|
||||
oldSiteResource: existingResource
|
||||
});
|
||||
} else {
|
||||
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -239,11 +221,8 @@ export async function updateClientResources(
|
||||
siteId: site.siteId,
|
||||
niceId: resourceNiceId,
|
||||
name: resourceData.name || resourceNiceId,
|
||||
mode: mappedMode.mode,
|
||||
ssl: mappedMode.ssl,
|
||||
scheme: mappedMode.scheme,
|
||||
mode: resourceData.mode,
|
||||
destination: resourceData.destination,
|
||||
destinationPort: resourceData["destination-port"],
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null,
|
||||
|
||||
@@ -325,11 +325,11 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
||||
export const ClientResourceSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
mode: z.enum(["host", "cidr", "http", "https"]),
|
||||
mode: z.enum(["host", "cidr"]),
|
||||
site: z.string(),
|
||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||
// proxyPort: z.int().positive().optional(),
|
||||
"destination-port": z.int().positive().optional(),
|
||||
// destinationPort: z.int().positive().optional(),
|
||||
destination: z.string().min(1),
|
||||
// enabled: z.boolean().default(true),
|
||||
"tcp-ports": portRangeStringSchema.optional().default("*"),
|
||||
|
||||
@@ -582,16 +582,6 @@ export type SubnetProxyTargetV2 = {
|
||||
protocol: "tcp" | "udp";
|
||||
}[];
|
||||
resourceId?: number;
|
||||
protocol?: "http" | "https"; // if set, this target only applies to the specified protocol
|
||||
httpTargets?: HTTPTarget[];
|
||||
tlsCert?: string;
|
||||
tlsKey?: string;
|
||||
};
|
||||
|
||||
export type HTTPTarget = {
|
||||
destAddr: string; // must be an IP or hostname
|
||||
destPort: number;
|
||||
scheme: "http" | "https";
|
||||
};
|
||||
|
||||
export function generateSubnetProxyTargetV2(
|
||||
@@ -629,7 +619,7 @@ export function generateSubnetProxyTargetV2(
|
||||
destPrefix: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId
|
||||
resourceId: siteResource.siteResourceId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -641,7 +631,7 @@ export function generateSubnetProxyTargetV2(
|
||||
rewriteTo: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId
|
||||
resourceId: siteResource.siteResourceId,
|
||||
};
|
||||
}
|
||||
} else if (siteResource.mode == "cidr") {
|
||||
@@ -650,46 +640,7 @@ export function generateSubnetProxyTargetV2(
|
||||
destPrefix: siteResource.destination,
|
||||
portRange,
|
||||
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
|
||||
target = {
|
||||
sourcePrefixes: [],
|
||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||
rewriteTo: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId,
|
||||
protocol: publicProtocol,
|
||||
httpTargets: [
|
||||
{
|
||||
destAddr: siteResource.destination,
|
||||
destPort: siteResource.destinationPort,
|
||||
scheme: siteResource.scheme
|
||||
}
|
||||
]
|
||||
// tlsCert: "",
|
||||
// tlsKey: ""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -719,31 +670,33 @@ export function generateSubnetProxyTargetV2(
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
||||
* by expanding each source prefix into its own target entry.
|
||||
* @param targetV2 - The v2 target to convert
|
||||
* @returns Array of v1 SubnetProxyTarget objects
|
||||
*/
|
||||
export function convertSubnetProxyTargetsV2ToV1(
|
||||
targetsV2: SubnetProxyTargetV2[]
|
||||
): SubnetProxyTarget[] {
|
||||
return targetsV2.flatMap((targetV2) =>
|
||||
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
||||
sourcePrefix,
|
||||
destPrefix: targetV2.destPrefix,
|
||||
...(targetV2.disableIcmp !== undefined && {
|
||||
disableIcmp: targetV2.disableIcmp
|
||||
}),
|
||||
...(targetV2.rewriteTo !== undefined && {
|
||||
rewriteTo: targetV2.rewriteTo
|
||||
}),
|
||||
...(targetV2.portRange !== undefined && {
|
||||
portRange: targetV2.portRange
|
||||
})
|
||||
}))
|
||||
);
|
||||
}
|
||||
export function convertSubnetProxyTargetsV2ToV1(
|
||||
targetsV2: SubnetProxyTargetV2[]
|
||||
): SubnetProxyTarget[] {
|
||||
return targetsV2.flatMap((targetV2) =>
|
||||
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
||||
sourcePrefix,
|
||||
destPrefix: targetV2.destPrefix,
|
||||
...(targetV2.disableIcmp !== undefined && {
|
||||
disableIcmp: targetV2.disableIcmp
|
||||
}),
|
||||
...(targetV2.rewriteTo !== undefined && {
|
||||
rewriteTo: targetV2.rewriteTo
|
||||
}),
|
||||
...(targetV2.portRange !== undefined && {
|
||||
portRange: targetV2.portRange
|
||||
})
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Custom schema for validating port range strings
|
||||
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
||||
|
||||
@@ -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({
|
||||
enable_redis: z.boolean().optional().default(false),
|
||||
use_pangolin_dns: z.boolean().optional().default(false),
|
||||
use_org_only_idp: z.boolean().optional(),
|
||||
enable_acme_cert_sync: z.boolean().optional().default(false)
|
||||
use_org_only_idp: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
.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
|
||||
.object({
|
||||
app_name: z.string().optional(),
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
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 {
|
||||
sanitize,
|
||||
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[] = [];
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
// create a list of all domains to get certs for
|
||||
@@ -304,12 +276,6 @@ export async function getTraefikConfig(
|
||||
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
|
||||
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
||||
// 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) {
|
||||
const exitNodeLoginPages = await db
|
||||
.select({
|
||||
|
||||
@@ -144,7 +144,7 @@ export async function getUserResources(
|
||||
name: string;
|
||||
destination: string;
|
||||
mode: string;
|
||||
scheme: string | null;
|
||||
protocol: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
@@ -156,7 +156,7 @@ export async function getUserResources(
|
||||
name: siteResources.name,
|
||||
destination: siteResources.destination,
|
||||
mode: siteResources.mode,
|
||||
scheme: siteResources.scheme,
|
||||
protocol: siteResources.protocol,
|
||||
enabled: siteResources.enabled,
|
||||
alias: siteResources.alias,
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
@@ -240,7 +240,7 @@ export async function getUserResources(
|
||||
name: siteResource.name,
|
||||
destination: siteResource.destination,
|
||||
mode: siteResource.mode,
|
||||
protocol: siteResource.scheme,
|
||||
protocol: siteResource.protocol,
|
||||
enabled: siteResource.enabled,
|
||||
alias: siteResource.alias,
|
||||
aliasAddress: siteResource.aliasAddress,
|
||||
@@ -289,7 +289,7 @@ export type GetUserResourcesResponse = {
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
type: "site";
|
||||
type: 'site';
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,12 +36,11 @@ const createSiteResourceParamsSchema = z.strictObject({
|
||||
const createSiteResourceSchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
mode: z.enum(["host", "cidr", "http"]),
|
||||
ssl: z.boolean().optional(), // only used for http mode
|
||||
mode: z.enum(["host", "cidr", "port"]),
|
||||
siteId: z.int(),
|
||||
scheme: z.enum(["http", "https"]).optional(),
|
||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||
// proxyPort: z.int().positive().optional(),
|
||||
destinationPort: z.int().positive().optional(),
|
||||
// destinationPort: z.int().positive().optional(),
|
||||
destination: z.string().min(1),
|
||||
enabled: z.boolean().default(true),
|
||||
alias: z
|
||||
@@ -63,20 +62,15 @@ const createSiteResourceSchema = z
|
||||
.strict()
|
||||
.refine(
|
||||
(data) => {
|
||||
if (
|
||||
data.mode === "host" ||
|
||||
data.mode == "http"
|
||||
) {
|
||||
if (data.mode == "host") {
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
if (data.mode === "host") {
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||
@@ -111,21 +105,6 @@ const createSiteResourceSchema = z
|
||||
{
|
||||
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>;
|
||||
@@ -182,12 +161,11 @@ export async function createSiteResource(
|
||||
name,
|
||||
siteId,
|
||||
mode,
|
||||
scheme,
|
||||
// protocol,
|
||||
// proxyPort,
|
||||
destinationPort,
|
||||
// destinationPort,
|
||||
destination,
|
||||
enabled,
|
||||
ssl,
|
||||
alias,
|
||||
userIds,
|
||||
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
|
||||
if (alias) {
|
||||
const [conflict] = await db
|
||||
@@ -278,7 +280,8 @@ export async function createSiteResource(
|
||||
|
||||
const niceId = await getUniqueSiteResourceName(orgId);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -290,11 +293,8 @@ export async function createSiteResource(
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
mode,
|
||||
ssl,
|
||||
mode: mode as "host" | "cidr",
|
||||
destination,
|
||||
scheme,
|
||||
destinationPort,
|
||||
enabled,
|
||||
alias,
|
||||
aliasAddress,
|
||||
|
||||
@@ -41,12 +41,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
||||
}),
|
||||
query: z.string().optional(),
|
||||
mode: z
|
||||
.enum(["host", "cidr", "http"])
|
||||
.enum(["host", "cidr"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["host", "cidr", "http"],
|
||||
enum: ["host", "cidr"],
|
||||
description: "Filter site resources by mode"
|
||||
}),
|
||||
sort_by: z
|
||||
@@ -88,8 +88,7 @@ function querySiteResourcesBase() {
|
||||
niceId: siteResources.niceId,
|
||||
name: siteResources.name,
|
||||
mode: siteResources.mode,
|
||||
ssl: siteResources.ssl,
|
||||
scheme: siteResources.scheme,
|
||||
protocol: siteResources.protocol,
|
||||
proxyPort: siteResources.proxyPort,
|
||||
destinationPort: siteResources.destinationPort,
|
||||
destination: siteResources.destination,
|
||||
@@ -194,9 +193,7 @@ export async function listAllSiteResourcesByOrg(
|
||||
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
||||
|
||||
const countQuery = db.$count(
|
||||
querySiteResourcesBase()
|
||||
.where(and(...conditions))
|
||||
.as("filtered_site_resources")
|
||||
querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources")
|
||||
);
|
||||
|
||||
const [siteResourcesList, totalCount] = await Promise.all([
|
||||
|
||||
@@ -51,11 +51,10 @@ const updateSiteResourceSchema = z
|
||||
)
|
||||
.optional(),
|
||||
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
||||
mode: z.enum(["host", "cidr", "http"]).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
scheme: z.enum(["http", "https"]).nullish(),
|
||||
mode: z.enum(["host", "cidr"]).optional(),
|
||||
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||
// proxyPort: z.int().positive().nullish(),
|
||||
destinationPort: z.int().positive().nullish(),
|
||||
// destinationPort: z.int().positive().nullish(),
|
||||
destination: z.string().min(1).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
alias: z
|
||||
@@ -77,20 +76,14 @@ const updateSiteResourceSchema = z
|
||||
.strict()
|
||||
.refine(
|
||||
(data) => {
|
||||
if (
|
||||
(data.mode === "host" ||
|
||||
data.mode == "http") &&
|
||||
data.destination
|
||||
) {
|
||||
if (data.mode == "host") {
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
if (data.mode === "host" && data.destination) {
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||
@@ -125,23 +118,6 @@ const updateSiteResourceSchema = z
|
||||
{
|
||||
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>;
|
||||
@@ -199,11 +175,8 @@ export async function updateSiteResource(
|
||||
siteId, // because it can change
|
||||
niceId,
|
||||
mode,
|
||||
scheme,
|
||||
destination,
|
||||
destinationPort,
|
||||
alias,
|
||||
ssl,
|
||||
enabled,
|
||||
userIds,
|
||||
roleIds,
|
||||
@@ -373,10 +346,7 @@ export async function updateSiteResource(
|
||||
siteId,
|
||||
niceId,
|
||||
mode,
|
||||
scheme,
|
||||
ssl,
|
||||
destination,
|
||||
destinationPort,
|
||||
enabled,
|
||||
alias: alias && alias.trim() ? alias : null,
|
||||
tcpPortRangeString,
|
||||
@@ -479,10 +449,7 @@ export async function updateSiteResource(
|
||||
name: name,
|
||||
siteId: siteId,
|
||||
mode: mode,
|
||||
scheme,
|
||||
ssl,
|
||||
destination: destination,
|
||||
destinationPort: destinationPort,
|
||||
enabled: enabled,
|
||||
alias: alias && alias.trim() ? alias : null,
|
||||
tcpPortRangeString: tcpPortRangeString,
|
||||
|
||||
@@ -235,9 +235,7 @@ export default async function migration() {
|
||||
for (const row of existingUserInviteRoles) {
|
||||
await db.execute(sql`
|
||||
INSERT INTO "userInviteRoles" ("inviteId", "roleId")
|
||||
SELECT ${row.inviteId}, ${row.roleId}
|
||||
WHERE EXISTS (SELECT 1 FROM "userInvites" WHERE "inviteId" = ${row.inviteId})
|
||||
AND EXISTS (SELECT 1 FROM "roles" WHERE "roleId" = ${row.roleId})
|
||||
VALUES (${row.inviteId}, ${row.roleId})
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
}
|
||||
@@ -260,10 +258,7 @@ export default async function migration() {
|
||||
for (const row of existingUserOrgRoles) {
|
||||
await db.execute(sql`
|
||||
INSERT INTO "userOrgRoles" ("userId", "orgId", "roleId")
|
||||
SELECT ${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})
|
||||
VALUES (${row.userId}, ${row.orgId}, ${row.roleId})
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export default async function migration() {
|
||||
).run();
|
||||
|
||||
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();
|
||||
db.prepare(`DROP TABLE 'userOrgs';`).run();
|
||||
db.prepare(
|
||||
@@ -246,15 +246,12 @@ export default async function migration() {
|
||||
// Re-insert the preserved invite role assignments into the new userInviteRoles table
|
||||
if (existingUserInviteRoles.length > 0) {
|
||||
const insertUserInviteRole = db.prepare(
|
||||
`INSERT OR IGNORE INTO 'userInviteRoles' ("inviteId", "roleId")
|
||||
SELECT ?, ?
|
||||
WHERE EXISTS (SELECT 1 FROM 'userInvites' WHERE inviteId = ?)
|
||||
AND EXISTS (SELECT 1 FROM 'roles' WHERE roleId = ?)`
|
||||
`INSERT OR IGNORE INTO 'userInviteRoles' ("inviteId", "roleId") VALUES (?, ?)`
|
||||
);
|
||||
|
||||
const insertAll = db.transaction(() => {
|
||||
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
|
||||
if (existingUserOrgRoles.length > 0) {
|
||||
const insertUserOrgRole = db.prepare(
|
||||
`INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId")
|
||||
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 = ?)`
|
||||
`INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId") VALUES (?, ?, ?)`
|
||||
);
|
||||
|
||||
const insertAll = db.transaction(() => {
|
||||
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(
|
||||
(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 {
|
||||
id: siteResource.siteResourceId,
|
||||
name: siteResource.name,
|
||||
orgId: params.orgId,
|
||||
siteName: siteResource.siteName,
|
||||
siteAddress: siteResource.siteAddress || null,
|
||||
mode: normalizedMode,
|
||||
scheme:
|
||||
siteResource.scheme ??
|
||||
(rawMode === "https" ? ("https" as const) : null),
|
||||
ssl:
|
||||
siteResource.ssl === true || rawMode === "https",
|
||||
mode: siteResource.mode || ("port" as any),
|
||||
// protocol: siteResource.protocol,
|
||||
// proxyPort: siteResource.proxyPort,
|
||||
siteId: siteResource.siteId,
|
||||
destination: siteResource.destination,
|
||||
httpHttpsPort: siteResource.destinationPort ?? null,
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
alias: siteResource.alias || null,
|
||||
aliasAddress: siteResource.aliasAddress || null,
|
||||
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;
|
||||
siteAddress: string | null;
|
||||
// mode: "host" | "cidr" | "port";
|
||||
mode: "host" | "cidr" | "http";
|
||||
scheme: "http" | "https" | null;
|
||||
ssl: boolean;
|
||||
mode: "host" | "cidr";
|
||||
// protocol: string | null;
|
||||
// proxyPort: number | null;
|
||||
siteId: number;
|
||||
siteNiceId: string;
|
||||
destination: string;
|
||||
httpHttpsPort: number | null;
|
||||
// destinationPort: number | null;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
niceId: string;
|
||||
@@ -65,39 +63,6 @@ export type InternalResourceRow = {
|
||||
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 = {
|
||||
internalResources: InternalResourceRow[];
|
||||
orgId: string;
|
||||
@@ -250,10 +215,6 @@ export default function ClientResourcesTable({
|
||||
{
|
||||
value: "cidr",
|
||||
label: t("editInternalResourceDialogModeCidr")
|
||||
},
|
||||
{
|
||||
value: "http",
|
||||
label: t("editInternalResourceDialogModeHttp")
|
||||
}
|
||||
]}
|
||||
selectedValue={searchParams.get("mode") ?? undefined}
|
||||
@@ -266,14 +227,10 @@ export default function ClientResourcesTable({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const modeLabels: Record<
|
||||
"host" | "cidr" | "port" | "http",
|
||||
string
|
||||
> = {
|
||||
const modeLabels: Record<"host" | "cidr" | "port", string> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
port: t("editInternalResourceDialogModePort"),
|
||||
http: t("editInternalResourceDialogModeHttp")
|
||||
port: t("editInternalResourceDialogModePort")
|
||||
};
|
||||
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||
}
|
||||
@@ -286,12 +243,11 @@ export default function ClientResourcesTable({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const display = formatDestinationDisplay(resourceRow);
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={display}
|
||||
text={resourceRow.destination}
|
||||
isLink={false}
|
||||
displayText={display}
|
||||
displayText={resourceRow.destination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -304,26 +260,15 @@ export default function ClientResourcesTable({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
if (resourceRow.mode === "host" && resourceRow.alias) {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={resourceRow.alias}
|
||||
isLink={false}
|
||||
displayText={resourceRow.alias}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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>;
|
||||
return resourceRow.mode === "host" && resourceRow.alias ? (
|
||||
<CopyToClipboard
|
||||
text={resourceRow.alias}
|
||||
isLink={false}
|
||||
displayText={resourceRow.alias}
|
||||
/>
|
||||
) : (
|
||||
<span>-</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,10 +50,7 @@ export default function CreateInternalResourceDialog({
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" || data.mode === "http") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
if (data.mode === "host" && isHostname(data.destination)) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
@@ -72,42 +69,21 @@ export default function CreateInternalResourceDialog({
|
||||
mode: data.mode,
|
||||
destination: data.destination,
|
||||
enabled: true,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.httpHttpsPort ?? undefined
|
||||
}),
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: undefined,
|
||||
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined,
|
||||
tcpPortRangeString: data.tcpPortRangeString,
|
||||
udpPortRangeString: data.udpPortRangeString,
|
||||
disableIcmp: data.disableIcmp ?? false,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" &&
|
||||
data.authDaemonPort != null && {
|
||||
authDaemonPort: data.authDaemonPort
|
||||
}),
|
||||
roleIds: data.roles
|
||||
? data.roles.map((r) => parseInt(r.id))
|
||||
: [],
|
||||
...(data.authDaemonMode != null && { authDaemonMode: data.authDaemonMode }),
|
||||
...(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) : [],
|
||||
clientIds: data.clients
|
||||
? data.clients.map((c) => parseInt(c.id))
|
||||
: []
|
||||
clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : []
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("createInternalResourceDialogSuccess"),
|
||||
description: t(
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully"
|
||||
),
|
||||
description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"),
|
||||
variant: "default"
|
||||
});
|
||||
setOpen(false);
|
||||
@@ -117,9 +93,7 @@ export default function CreateInternalResourceDialog({
|
||||
title: t("createInternalResourceDialogError"),
|
||||
description: formatAxiosError(
|
||||
error,
|
||||
t(
|
||||
"createInternalResourceDialogFailedToCreateInternalResource"
|
||||
)
|
||||
t("createInternalResourceDialogFailedToCreateInternalResource")
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -132,13 +106,9 @@ export default function CreateInternalResourceDialog({
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-3xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("createInternalResourceDialogCreateClientResource")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaTitle>{t("createInternalResourceDialogCreateClientResource")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t(
|
||||
"createInternalResourceDialogCreateClientResourceDescription"
|
||||
)}
|
||||
{t("createInternalResourceDialogCreateClientResourceDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -153,11 +123,7 @@ export default function CreateInternalResourceDialog({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
|
||||
{t("createInternalResourceDialogCancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
|
||||
@@ -163,18 +163,15 @@ export default function DomainPicker({
|
||||
domainId: firstOrExistingDomain.domainId
|
||||
};
|
||||
|
||||
const base = firstOrExistingDomain.baseDomain;
|
||||
const sub =
|
||||
firstOrExistingDomain.type !== "cname"
|
||||
? defaultSubdomain?.trim() || undefined
|
||||
: undefined;
|
||||
|
||||
onDomainChange?.({
|
||||
domainId: firstOrExistingDomain.domainId,
|
||||
type: "organization",
|
||||
subdomain: sub,
|
||||
fullDomain: sub ? `${sub}.${base}` : base,
|
||||
baseDomain: base
|
||||
subdomain:
|
||||
firstOrExistingDomain.type !== "cname"
|
||||
? defaultSubdomain || undefined
|
||||
: undefined,
|
||||
fullDomain: firstOrExistingDomain.baseDomain,
|
||||
baseDomain: firstOrExistingDomain.baseDomain
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -512,11 +509,9 @@ export default function DomainPicker({
|
||||
<span className="truncate">
|
||||
{selectedBaseDomain.domain}
|
||||
</span>
|
||||
{selectedBaseDomain.verified &&
|
||||
selectedBaseDomain.domainType !==
|
||||
"wildcard" && (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
||||
)}
|
||||
{selectedBaseDomain.verified && (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
t("domainPickerSelectBaseDomain")
|
||||
@@ -579,23 +574,14 @@ export default function DomainPicker({
|
||||
}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{orgDomain.type ===
|
||||
"wildcard"
|
||||
{orgDomain.type.toUpperCase()}{" "}
|
||||
•{" "}
|
||||
{orgDomain.verified
|
||||
? t(
|
||||
"domainPickerManual"
|
||||
"domainPickerVerified"
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{orgDomain.type.toUpperCase()}{" "}
|
||||
•{" "}
|
||||
{orgDomain.verified
|
||||
? t(
|
||||
"domainPickerVerified"
|
||||
)
|
||||
: t(
|
||||
"domainPickerUnverified"
|
||||
)}
|
||||
</>
|
||||
: t(
|
||||
"domainPickerUnverified"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -54,10 +54,7 @@ export default function EditInternalResourceDialog({
|
||||
async function handleSubmit(values: InternalResourceFormValues) {
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" || data.mode === "http") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
if (data.mode === "host" && isHostname(data.destination)) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
@@ -74,11 +71,6 @@ export default function EditInternalResourceDialog({
|
||||
mode: data.mode,
|
||||
niceId: data.niceId,
|
||||
destination: data.destination,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.httpHttpsPort ?? null
|
||||
}),
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user