Merge pull request #3756 from fosrl/dev

1.23.0
This commit is contained in:
Owen Schwartz
2026-09-15 16:48:42 -04:00
committed by GitHub
49 changed files with 1889 additions and 1642 deletions
+4
View File
@@ -0,0 +1,4 @@
# typescript-eslint@8.70.0 declares a peer range of typescript "<6.1.0" and
# hasn't caught up to typescript@7.x yet, even though it works fine against it
# in practice. Without this, `npm install`/`npm ci` fail with ERESOLVE.
legacy-peer-deps=true
+1 -1
View File
@@ -5,7 +5,7 @@ WORKDIR /app
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY package*.json ./ COPY package*.json .npmrc ./
FROM base AS builder-dev FROM base AS builder-dev
+1 -1
View File
@@ -4,7 +4,7 @@ WORKDIR /app
RUN apk add --no-cache python3 make g++ RUN apk add --no-cache python3 make g++
COPY package*.json ./ COPY package*.json .npmrc ./
# Install dependencies # Install dependencies
RUN npm ci RUN npm ci
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Няма намерени сайтове.", "sitesNotFound": "Няма намерени сайтове.",
"pangolinServerAdmin": "Администратор на сървър - Панголин", "pangolinServerAdmin": "Администратор на сървър - Панголин",
"licenseTierProfessional": "Професионален лиценз", "licenseTierProfessional": "Професионален лиценз",
"licenseTierEnterprise": "Предприятие лиценз", "licenseTierEnterprise": "Предприятие",
"licenseTierPersonal": "Персонален лиценз", "licenseTierPersonal": "Личен",
"licenseTierTier1": "Начален",
"licenseTierTier2": "Мащаб",
"licensed": "Лицензиран", "licensed": "Лицензиран",
"yes": "Да", "yes": "Да",
"no": "Не", "no": "Не",
+5 -3
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Nebyly nalezeny žádné stránky.", "sitesNotFound": "Nebyly nalezeny žádné stránky.",
"pangolinServerAdmin": "Správce serveru - Pangolin", "pangolinServerAdmin": "Správce serveru - Pangolin",
"licenseTierProfessional": "Profesionální licence", "licenseTierProfessional": "Profesionální licence",
"licenseTierEnterprise": "Podniková licence", "licenseTierEnterprise": "Podniky",
"licenseTierPersonal": "Osobní licence", "licenseTierPersonal": "Osobní",
"licenseTierTier1": "Počáteční",
"licenseTierTier2": "Měřítko",
"licensed": "Licencováno", "licensed": "Licencováno",
"yes": "Ano", "yes": "Ano",
"no": "Ne", "no": "Ne",
@@ -3979,7 +3981,7 @@
"tpmAvailable": "TPM k dispozici", "tpmAvailable": "TPM k dispozici",
"windowsAntivirusEnabled": "Antivirus povolen", "windowsAntivirusEnabled": "Antivirus povolen",
"macosSipEnabled": "Ochrana systémové integrity (SIP)", "macosSipEnabled": "Ochrana systémové integrity (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Strážce",
"macosFirewallStealthMode": "Režim neviditelnosti firewallu", "macosFirewallStealthMode": "Režim neviditelnosti firewallu",
"linuxAppArmorEnabled": "Pancíř aplikace", "linuxAppArmorEnabled": "Pancíř aplikace",
"linuxSELinuxEnabled": "SELinux", "linuxSELinuxEnabled": "SELinux",
+5 -3
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Ingen sites fundet.", "sitesNotFound": "Ingen sites fundet.",
"pangolinServerAdmin": "Serveradmin - Pangolin", "pangolinServerAdmin": "Serveradmin - Pangolin",
"licenseTierProfessional": "Professionel licens", "licenseTierProfessional": "Professionel licens",
"licenseTierEnterprise": "Enterprise-licens", "licenseTierEnterprise": "Bedrift",
"licenseTierPersonal": "Personlig licens", "licenseTierPersonal": "Personlig",
"licenseTierTier1": "Begynder",
"licenseTierTier2": "Skala",
"licensed": "Licenseret", "licensed": "Licenseret",
"yes": "Ja", "yes": "Ja",
"no": "Nej", "no": "Nej",
@@ -2068,7 +2070,7 @@
"aiUsageFilterNotFound": "Ingen valg fundet", "aiUsageFilterNotFound": "Ingen valg fundet",
"aiUsageResetFilters": "Nulstil Filtre", "aiUsageResetFilters": "Nulstil Filtre",
"aiUsageRefresh": "Opdater", "aiUsageRefresh": "Opdater",
"aiUsageTokenTypePrompt": "Prompt", "aiUsageTokenTypePrompt": "Vis",
"aiUsageTokenTypeCacheRead": "Cache læs", "aiUsageTokenTypeCacheRead": "Cache læs",
"aiUsageTokenTypeCacheWrite": "Cache skriv", "aiUsageTokenTypeCacheWrite": "Cache skriv",
"aiUsageTokenTypeCompletion": "Komplettering", "aiUsageTokenTypeCompletion": "Komplettering",
+5 -3
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Keine Standorte gefunden.", "sitesNotFound": "Keine Standorte gefunden.",
"pangolinServerAdmin": "Server-Admin - Pangolin", "pangolinServerAdmin": "Server-Admin - Pangolin",
"licenseTierProfessional": "Professional Lizenz", "licenseTierProfessional": "Professional Lizenz",
"licenseTierEnterprise": "Enterprise Lizenz", "licenseTierEnterprise": "Firma",
"licenseTierPersonal": "Persönliche Lizenz", "licenseTierPersonal": "Persönlich",
"licenseTierTier1": "Starter",
"licenseTierTier2": "Maßstab",
"licensed": "Lizenziert", "licensed": "Lizenziert",
"yes": "Ja", "yes": "Ja",
"no": "Nein", "no": "Nein",
@@ -3205,7 +3207,7 @@
"idpAzureClientIdDescription2": "Azure App Registration Client ID", "idpAzureClientIdDescription2": "Azure App Registration Client ID",
"idpAzureClientSecretDescription2": "Azure App Registration Client Geheimnis", "idpAzureClientSecretDescription2": "Azure App Registration Client Geheimnis",
"idpGoogleDescription": "Google OAuth2/OIDC Provider", "idpGoogleDescription": "Google OAuth2/OIDC Provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC-Anbieter",
"subnet": "Subnetz", "subnet": "Subnetz",
"utilitySubnet": "Nutzsubnetz", "utilitySubnet": "Nutzsubnetz",
"subnetDescription": "Das Subnetz für die Netzwerkkonfiguration dieser Organisation.", "subnetDescription": "Das Subnetz für die Netzwerkkonfiguration dieser Organisation.",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "No sites found.", "sitesNotFound": "No sites found.",
"pangolinServerAdmin": "Server Admin - Pangolin", "pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Professional License", "licenseTierProfessional": "Professional License",
"licenseTierEnterprise": "Enterprise License", "licenseTierEnterprise": "Enterprise",
"licenseTierPersonal": "Personal License", "licenseTierPersonal": "Personal",
"licenseTierTier1": "Starter",
"licenseTierTier2": "Scale",
"licensed": "Licensed", "licensed": "Licensed",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Sitios no encontrados.", "sitesNotFound": "Sitios no encontrados.",
"pangolinServerAdmin": "Admin Servidor - Pangolin", "pangolinServerAdmin": "Admin Servidor - Pangolin",
"licenseTierProfessional": "Licencia profesional", "licenseTierProfessional": "Licencia profesional",
"licenseTierEnterprise": "Licencia Enterprise", "licenseTierEnterprise": "Empresa",
"licenseTierPersonal": "Licencia personal", "licenseTierPersonal": "Uso personal",
"licenseTierTier1": "Iniciador",
"licenseTierTier2": "Escala",
"licensed": "Licenciado", "licensed": "Licenciado",
"yes": "Sí", "yes": "Sí",
"no": "Nu", "no": "Nu",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Aucun site trouvé.", "sitesNotFound": "Aucun site trouvé.",
"pangolinServerAdmin": "Admin Serveur - Pangolin", "pangolinServerAdmin": "Admin Serveur - Pangolin",
"licenseTierProfessional": "Licence Professionnelle", "licenseTierProfessional": "Licence Professionnelle",
"licenseTierEnterprise": "Licence Entreprise", "licenseTierEnterprise": "Entreprise",
"licenseTierPersonal": "Licence personnelle", "licenseTierPersonal": "Personnel",
"licenseTierTier1": "Démarrage",
"licenseTierTier2": "Échelle",
"licensed": "Sous licence", "licensed": "Sous licence",
"yes": "Oui", "yes": "Oui",
"no": "Non", "no": "Non",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Nessun sito trovato.", "sitesNotFound": "Nessun sito trovato.",
"pangolinServerAdmin": "Server Admin - Pangolina", "pangolinServerAdmin": "Server Admin - Pangolina",
"licenseTierProfessional": "Licenza Professional", "licenseTierProfessional": "Licenza Professional",
"licenseTierEnterprise": "Licenza Enterprise", "licenseTierEnterprise": "Impresa",
"licenseTierPersonal": "Licenza Personale", "licenseTierPersonal": "Personale",
"licenseTierTier1": "Avviatore",
"licenseTierTier2": "Scala",
"licensed": "Con Licenza", "licensed": "Con Licenza",
"yes": "Sì", "yes": "Sì",
"no": "No", "no": "No",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "사이트를 찾을 수 없습니다.", "sitesNotFound": "사이트를 찾을 수 없습니다.",
"pangolinServerAdmin": "서버 관리자 - 판골린", "pangolinServerAdmin": "서버 관리자 - 판골린",
"licenseTierProfessional": "전문 라이센스", "licenseTierProfessional": "전문 라이센스",
"licenseTierEnterprise": "기업 라이선스", "licenseTierEnterprise": "기업",
"licenseTierPersonal": "개인 라이선스", "licenseTierPersonal": "개인",
"licenseTierTier1": "스타터",
"licenseTierTier2": "스케일",
"licensed": "라이센스", "licensed": "라이센스",
"yes": "예", "yes": "예",
"no": "아니요", "no": "아니요",
+20 -18
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Ingen områder funnet.", "sitesNotFound": "Ingen områder funnet.",
"pangolinServerAdmin": "Server Admin - Pangolin", "pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Profesjonell lisens", "licenseTierProfessional": "Profesjonell lisens",
"licenseTierEnterprise": "Bedriftslisens", "licenseTierEnterprise": "Bedrift",
"licenseTierPersonal": "Personlig lisens", "licenseTierPersonal": "Personlig",
"licenseTierTier1": "Begynner",
"licenseTierTier2": "Skala",
"licensed": "Lisensiert", "licensed": "Lisensiert",
"yes": "Ja", "yes": "Ja",
"no": "Nei", "no": "Nei",
@@ -2816,7 +2818,7 @@
"roleTextImportPreview": "Forhåndsvisning", "roleTextImportPreview": "Forhåndsvisning",
"roleTextImportItemCount": "{count, plural, =0 {Ingen elementer å importere} one {ett element å importere} other {# elementer å importere}}", "roleTextImportItemCount": "{count, plural, =0 {Ingen elementer å importere} one {ett element å importere} other {# elementer å importere}}",
"roleTextImportTotalCount": "{existing} eksisterende + {imported} importert = {total} totalt", "roleTextImportTotalCount": "{existing} eksisterende + {imported} importert = {total} totalt",
"roleTextImportConfirm": "Import", "roleTextImportConfirm": "Importer",
"roleTextImportInvalidFile": "Ustøttet filtype", "roleTextImportInvalidFile": "Ustøttet filtype",
"roleTextImportInvalidFileDescription": "Bare .txt og .csv filer er støttet.", "roleTextImportInvalidFileDescription": "Bare .txt og .csv filer er støttet.",
"roleTextImportEmpty": "Ingen elementer funnet i filen", "roleTextImportEmpty": "Ingen elementer funnet i filen",
@@ -3093,7 +3095,7 @@
"regionAfrica": "Afrika", "regionAfrica": "Afrika",
"regionNorthernAfrica": "[country name] Nord-Afrika", "regionNorthernAfrica": "[country name] Nord-Afrika",
"regionEasternAfrica": "Øst-Afrika", "regionEasternAfrica": "Øst-Afrika",
"regionMiddleAfrica": "Middle Africa", "regionMiddleAfrica": "Midt-Afrika",
"regionSouthernAfrica": "Sør-Afrika", "regionSouthernAfrica": "Sør-Afrika",
"regionWesternAfrica": "[country name] Vest-Afrika", "regionWesternAfrica": "[country name] Vest-Afrika",
"regionAmericas": "Amerika", "regionAmericas": "Amerika",
@@ -3112,10 +3114,10 @@
"regionNorthernEurope": "Nord-Europa", "regionNorthernEurope": "Nord-Europa",
"regionSouthernEurope": "Sørlige Europa", "regionSouthernEurope": "Sørlige Europa",
"regionWesternEurope": "Vest-Europa", "regionWesternEurope": "Vest-Europa",
"regionOceania": "Oceania", "regionOceania": "Oseania",
"regionAustraliaAndNewZealand": "Australia og New Zealand", "regionAustraliaAndNewZealand": "Australia og New Zealand",
"regionMelanesia": "Melanesia", "regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia", "regionMicronesia": "Mikronesia",
"regionPolynesia": "Polynesia", "regionPolynesia": "Polynesia",
"managedSelfHosted": { "managedSelfHosted": {
"title": "Administrert selv-hostet", "title": "Administrert selv-hostet",
@@ -3205,7 +3207,7 @@
"idpAzureClientIdDescription2": "Azure App registrerings klient-ID", "idpAzureClientIdDescription2": "Azure App registrerings klient-ID",
"idpAzureClientSecretDescription2": "Azure App Registrering Klient Hemmelig", "idpAzureClientSecretDescription2": "Azure App Registrering Klient Hemmelig",
"idpGoogleDescription": "Google OAuth2/OIDC leverandør", "idpGoogleDescription": "Google OAuth2/OIDC leverandør",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC-leverandør",
"subnet": "Subnett", "subnet": "Subnett",
"utilitySubnet": "Nyttesubnett", "utilitySubnet": "Nyttesubnett",
"subnetDescription": "Undernettverket for denne organisasjonens nettverkskonfigurasjon.", "subnetDescription": "Undernettverket for denne organisasjonens nettverkskonfigurasjon.",
@@ -3220,7 +3222,7 @@
"authPageBrandingRemoveTitle": "Fjern markedsføring for autentiseringsside", "authPageBrandingRemoveTitle": "Fjern markedsføring for autentiseringsside",
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?", "authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging", "authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo-URL",
"brandingLogoURLOrPath": "Logoen URL eller sti", "brandingLogoURLOrPath": "Logoen URL eller sti",
"brandingLogoPathDescription": "Skriv inn en URL eller en lokal bane.", "brandingLogoPathDescription": "Skriv inn en URL eller en lokal bane.",
"brandingLogoURLDescription": "Skriv inn en offentlig tilgjengelig nettadresse til din logobilde.", "brandingLogoURLDescription": "Skriv inn en offentlig tilgjengelig nettadresse til din logobilde.",
@@ -3360,7 +3362,7 @@
"resourceHeaderAuthSetupTitleDescription": "Angi grunnleggende auth legitimasjon (brukernavn og passord) for å beskytte denne ressursen med HTTP Header autentisering. Tilgang til det ved hjelp av formatet https://username:password@resource.example.com", "resourceHeaderAuthSetupTitleDescription": "Angi grunnleggende auth legitimasjon (brukernavn og passord) for å beskytte denne ressursen med HTTP Header autentisering. Tilgang til det ved hjelp av formatet https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Angi topptekst godkjenning", "resourceHeaderAuthSubmit": "Angi topptekst godkjenning",
"actionSetResourceHeaderAuth": "Angi topptekst godkjenning", "actionSetResourceHeaderAuth": "Angi topptekst godkjenning",
"enterpriseEdition": "Enterprise Edition", "enterpriseEdition": "Enterprise-utgave",
"unlicensed": "Ikke lisensiert", "unlicensed": "Ikke lisensiert",
"beta": "beta", "beta": "beta",
"manageUserDevices": "Bruker Enheter", "manageUserDevices": "Bruker Enheter",
@@ -3501,7 +3503,7 @@
"priority": "Prioritet", "priority": "Prioritet",
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.", "priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
"instanceName": "Forekomst navn", "instanceName": "Forekomst navn",
"clearInstanceName": "Reset Server Association", "clearInstanceName": "Tilbakestill server-assosiasjon",
"pathMatchModalTitle": "Konfigurere matching av sti", "pathMatchModalTitle": "Konfigurere matching av sti",
"pathMatchModalDescription": "Sett opp hvordan innkommende forespørsler skal matches basert på deres bane.", "pathMatchModalDescription": "Sett opp hvordan innkommende forespørsler skal matches basert på deres bane.",
"pathMatchType": "Trefftype", "pathMatchType": "Trefftype",
@@ -3557,11 +3559,11 @@
"allowedByRule": "Tillatt etter regel", "allowedByRule": "Tillatt etter regel",
"allowedNoAuth": "Tillatt Ingen Auth", "allowedNoAuth": "Tillatt Ingen Auth",
"validAccessToken": "Gyldig tilgangsnøkkel", "validAccessToken": "Gyldig tilgangsnøkkel",
"validHeaderAuth": "Valid header auth", "validHeaderAuth": "Gyldig header-autentisering",
"validPincode": "Gyldig PIN-kode", "validPincode": "Gyldig PIN-kode",
"validPassword": "Gyldig passord", "validPassword": "Gyldig passord",
"validEmail": "Valid email", "validEmail": "Gyldig e-post",
"validSSO": "Valid SSO", "validSSO": "Gyldig SSO",
"validVirtualAPIKey": "Gyldig Virtuell API-nøkkel", "validVirtualAPIKey": "Gyldig Virtuell API-nøkkel",
"view": "Vis", "view": "Vis",
"configManaged": "Konfigurasjon administrert", "configManaged": "Konfigurasjon administrert",
@@ -3570,7 +3572,7 @@
"droppedByRule": "Legg i regelen", "droppedByRule": "Legg i regelen",
"noSessions": "Ingen økter", "noSessions": "Ingen økter",
"temporaryRequestToken": "Midlertidig forespørsel Token", "temporaryRequestToken": "Midlertidig forespørsel Token",
"noMoreAuthMethods": "No Valid Auth", "noMoreAuthMethods": "Ingen gyldig autentisering",
"ip": "IP", "ip": "IP",
"reason": "Grunn", "reason": "Grunn",
"requestLogs": "HTTP-forespørselslogger", "requestLogs": "HTTP-forespørselslogger",
@@ -3784,14 +3786,14 @@
"niceIdUpdateErrorDescription": "Det oppstod en feil under oppdatering av Nice ID.", "niceIdUpdateErrorDescription": "Det oppstod en feil under oppdatering av Nice ID.",
"niceIdCannotBeEmpty": "God ID kan ikke være tom", "niceIdCannotBeEmpty": "God ID kan ikke være tom",
"enterIdentifier": "Angi identifikator", "enterIdentifier": "Angi identifikator",
"identifier": "Identifier", "identifier": "Identifikator",
"deviceLoginUseDifferentAccount": "Ikke du? Bruk en annen konto.", "deviceLoginUseDifferentAccount": "Ikke du? Bruk en annen konto.",
"deviceLoginDeviceRequestingAccessToAccount": "En enhet ber om tilgang til denne kontoen.", "deviceLoginDeviceRequestingAccessToAccount": "En enhet ber om tilgang til denne kontoen.",
"loginSelectAuthenticationMethod": "Velg en autentiseringsmetode for å fortsette.", "loginSelectAuthenticationMethod": "Velg en autentiseringsmetode for å fortsette.",
"noData": "Ingen data", "noData": "Ingen data",
"machineClients": "Maskinklienter", "machineClients": "Maskinklienter",
"install": "Installer", "install": "Installer",
"downloadInstaller": "Download Installer", "downloadInstaller": "Last ned installasjonsprogram",
"run": "Kjør", "run": "Kjør",
"envFile": "Miljøfil", "envFile": "Miljøfil",
"serviceFile": "Tjenestefil", "serviceFile": "Tjenestefil",
@@ -3969,7 +3971,7 @@
"kernelVersion": "Kjerne versjon", "kernelVersion": "Kjerne versjon",
"deviceModel": "Enhets modell", "deviceModel": "Enhets modell",
"serialNumber": "Serienummer", "serialNumber": "Serienummer",
"hostname": "Hostname", "hostname": "Vertsnavn",
"firstSeen": "Først sett", "firstSeen": "Først sett",
"lastSeen": "Sist sett", "lastSeen": "Sist sett",
"biometricsEnabled": "Biometri aktivert", "biometricsEnabled": "Biometri aktivert",
@@ -3999,7 +4001,7 @@
"disconnected": "Frakoblet", "disconnected": "Frakoblet",
"approvalsEmptyStateTitle": "Enhetsgodkjenninger er ikke aktivert", "approvalsEmptyStateTitle": "Enhetsgodkjenninger er ikke aktivert",
"approvalsEmptyStateDescription": "Aktivere godkjenninger av enheter for at roller må godkjennes av admin før brukere kan koble til nye enheter.", "approvalsEmptyStateDescription": "Aktivere godkjenninger av enheter for at roller må godkjennes av admin før brukere kan koble til nye enheter.",
"approvalsEmptyStateHowToTitle": "How to Enable", "approvalsEmptyStateHowToTitle": "Hvordan aktivere",
"approvalsEmptyStateStep1Title": "Gå til roller", "approvalsEmptyStateStep1Title": "Gå til roller",
"approvalsEmptyStateStep1Description": "Naviger til organisasjonens roller innstillinger for å konfigurere enhetsgodkjenninger.", "approvalsEmptyStateStep1Description": "Naviger til organisasjonens roller innstillinger for å konfigurere enhetsgodkjenninger.",
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger", "approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
+5 -3
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Geen sites gevonden.", "sitesNotFound": "Geen sites gevonden.",
"pangolinServerAdmin": "Serverbeheer - Pangolin", "pangolinServerAdmin": "Serverbeheer - Pangolin",
"licenseTierProfessional": "Professionele licentie", "licenseTierProfessional": "Professionele licentie",
"licenseTierEnterprise": "Enterprise Licentie", "licenseTierEnterprise": "Onderneming",
"licenseTierPersonal": "Persoonlijke licentie", "licenseTierPersonal": "Persoonlijk",
"licenseTierTier1": "Beginner",
"licenseTierTier2": "Schaal",
"licensed": "Gelicentieerd", "licensed": "Gelicentieerd",
"yes": "ja", "yes": "ja",
"no": "Neen", "no": "Neen",
@@ -3205,7 +3207,7 @@
"idpAzureClientIdDescription2": "Azure App registratie Client ID", "idpAzureClientIdDescription2": "Azure App registratie Client ID",
"idpAzureClientSecretDescription2": "Azure App registratie client geheim", "idpAzureClientSecretDescription2": "Azure App registratie client geheim",
"idpGoogleDescription": "Algemene OAuth2/OIDC provider", "idpGoogleDescription": "Algemene OAuth2/OIDC provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC-provider",
"subnet": "Subnet", "subnet": "Subnet",
"utilitySubnet": "Hulpmiddel Subnet", "utilitySubnet": "Hulpmiddel Subnet",
"subnetDescription": "Het subnet van de netwerkconfiguratie van deze organisatie.", "subnetDescription": "Het subnet van de netwerkconfiguratie van deze organisatie.",
+6 -4
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Nie znaleziono witryn.", "sitesNotFound": "Nie znaleziono witryn.",
"pangolinServerAdmin": "Administrator serwera - Pangolin", "pangolinServerAdmin": "Administrator serwera - Pangolin",
"licenseTierProfessional": "Licencja Professional", "licenseTierProfessional": "Licencja Professional",
"licenseTierEnterprise": "Licencja Enterprise", "licenseTierEnterprise": "Przedsiębiorstwo",
"licenseTierPersonal": "Licencja osobista", "licenseTierPersonal": "Osobiste",
"licenseTierTier1": "Startowy",
"licenseTierTier2": "Skala",
"licensed": "Licencjonowany", "licensed": "Licencjonowany",
"yes": "Tak", "yes": "Tak",
"no": "Nie", "no": "Nie",
@@ -3791,7 +3793,7 @@
"noData": "Brak danych", "noData": "Brak danych",
"machineClients": "Klienci maszyn", "machineClients": "Klienci maszyn",
"install": "Zainstaluj", "install": "Zainstaluj",
"downloadInstaller": "Download Installer", "downloadInstaller": "Pobierz instalator",
"run": "Uruchom", "run": "Uruchom",
"envFile": "Plik środowiska", "envFile": "Plik środowiska",
"serviceFile": "Plik serwisu", "serviceFile": "Plik serwisu",
@@ -3999,7 +4001,7 @@
"disconnected": "Rozłączony", "disconnected": "Rozłączony",
"approvalsEmptyStateTitle": "Zatwierdzanie urządzenia nie włączone", "approvalsEmptyStateTitle": "Zatwierdzanie urządzenia nie włączone",
"approvalsEmptyStateDescription": "Włącz zatwierdzanie urządzeń dla ról aby wymagać zgody administratora, zanim użytkownicy będą mogli podłączyć nowe urządzenia.", "approvalsEmptyStateDescription": "Włącz zatwierdzanie urządzeń dla ról aby wymagać zgody administratora, zanim użytkownicy będą mogli podłączyć nowe urządzenia.",
"approvalsEmptyStateHowToTitle": "How to Enable", "approvalsEmptyStateHowToTitle": "Jak włączyć",
"approvalsEmptyStateStep1Title": "Przejdź do ról", "approvalsEmptyStateStep1Title": "Przejdź do ról",
"approvalsEmptyStateStep1Description": "Przejdź do ustawień ról swojej organizacji, aby skonfigurować zatwierdzenia urządzenia.", "approvalsEmptyStateStep1Description": "Przejdź do ustawień ról swojej organizacji, aby skonfigurować zatwierdzenia urządzenia.",
"approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia", "approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Nenhum site encontrado.", "sitesNotFound": "Nenhum site encontrado.",
"pangolinServerAdmin": "Administrador do Servidor - Pangolin", "pangolinServerAdmin": "Administrador do Servidor - Pangolin",
"licenseTierProfessional": "Licença Profissional", "licenseTierProfessional": "Licença Profissional",
"licenseTierEnterprise": "Licença Empresarial", "licenseTierEnterprise": "Empresa",
"licenseTierPersonal": "Licença Pessoal", "licenseTierPersonal": "Pessoal",
"licenseTierTier1": "Iniciante",
"licenseTierTier2": "Escala",
"licensed": "Licenciado", "licensed": "Licenciado",
"yes": "Sim", "yes": "Sim",
"no": "Não", "no": "Não",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Сайты не найдены.", "sitesNotFound": "Сайты не найдены.",
"pangolinServerAdmin": "Администратор сервера - Pangolin", "pangolinServerAdmin": "Администратор сервера - Pangolin",
"licenseTierProfessional": "Профессиональная лицензия", "licenseTierProfessional": "Профессиональная лицензия",
"licenseTierEnterprise": "Корпоративная лицензия", "licenseTierEnterprise": "Предприятие",
"licenseTierPersonal": "Личная лицензия", "licenseTierPersonal": "Личное",
"licenseTierTier1": "Старт",
"licenseTierTier2": "Масштаб",
"licensed": "Лицензировано", "licensed": "Лицензировано",
"yes": "Да", "yes": "Да",
"no": "Нет", "no": "Нет",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "Site bulunamadı.", "sitesNotFound": "Site bulunamadı.",
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin", "pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
"licenseTierProfessional": "Profesyonel Lisans", "licenseTierProfessional": "Profesyonel Lisans",
"licenseTierEnterprise": "Kurumsal Lisans", "licenseTierEnterprise": "Kurumsal",
"licenseTierPersonal": "Kişisel Lisans", "licenseTierPersonal": "Kişisel",
"licenseTierTier1": "Başlangıç",
"licenseTierTier2": "Ölçek",
"licensed": "Lisanslı", "licensed": "Lisanslı",
"yes": "Evet", "yes": "Evet",
"no": "Hayır", "no": "Hayır",
+4 -2
View File
@@ -1118,8 +1118,10 @@
"sitesNotFound": "未找到站点。", "sitesNotFound": "未找到站点。",
"pangolinServerAdmin": "服务器管理 - Pangolin", "pangolinServerAdmin": "服务器管理 - Pangolin",
"licenseTierProfessional": "专业许可证", "licenseTierProfessional": "专业许可证",
"licenseTierEnterprise": "企业许可证", "licenseTierEnterprise": "企业",
"licenseTierPersonal": "个人许可证", "licenseTierPersonal": "个人",
"licenseTierTier1": "启动器",
"licenseTierTier2": "扩展",
"licensed": "已授权", "licensed": "已授权",
"yes": "是", "yes": "是",
"no": "否", "no": "否",
+1530 -1394
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -69,7 +69,7 @@
"@simplewebauthn/server": "14.0.1", "@simplewebauthn/server": "14.0.1",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.102.8", "@tanstack/react-query": "5.102.8",
"@tanstack/react-table": "9.2.4", "@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
+13 -9
View File
@@ -1,5 +1,6 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile"; import { readConfigFile } from "@server/lib/readConfigFile";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
import { withReplicas } from "drizzle-orm/pg-core"; import { withReplicas } from "drizzle-orm/pg-core";
import { createPool } from "./poolConfig"; import { createPool } from "./poolConfig";
@@ -7,17 +8,20 @@ function createDb() {
const config = readConfigFile(); const config = readConfigFile();
// check the environment variables for postgres config first before the config file // check the environment variables for postgres config first before the config file
if (process.env.POSTGRES_CONNECTION_STRING) { const envConnectionString = readEnvOrFile("POSTGRES_CONNECTION_STRING");
if (envConnectionString) {
config.postgres = { config.postgres = {
connection_string: process.env.POSTGRES_CONNECTION_STRING connection_string: envConnectionString
}; };
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { const replicaConnectionStrings = readEnvOrFile(
const replicas = "POSTGRES_REPLICA_CONNECTION_STRINGS"
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( );
(conn) => ({ if (replicaConnectionStrings) {
connection_string: conn.trim() const replicas = replicaConnectionStrings
}) .split(",")
); .map((conn) => ({
connection_string: conn.trim()
}));
config.postgres.replicas = replicas; config.postgres.replicas = replicas;
} }
} }
+11 -8
View File
@@ -1,5 +1,6 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile"; import { readConfigFile } from "@server/lib/readConfigFile";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
import { withReplicas } from "drizzle-orm/pg-core"; import { withReplicas } from "drizzle-orm/pg-core";
import { build } from "@server/build"; import { build } from "@server/build";
import { db as mainDb } from "./driver"; import { db as mainDb } from "./driver";
@@ -17,7 +18,7 @@ function createLogsDb() {
const logsConfig = config.postgres_logs; const logsConfig = config.postgres_logs;
// Check environment variable first // Check environment variable first
let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING; let connectionString = readEnvOrFile("POSTGRES_LOGS_CONNECTION_STRING");
let replicaConnections: Array<{ connection_string: string }> = []; let replicaConnections: Array<{ connection_string: string }> = [];
if (!connectionString && logsConfig) { if (!connectionString && logsConfig) {
@@ -26,13 +27,15 @@ function createLogsDb() {
} }
// If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it // If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it
if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) { const replicaConnectionStrings = readEnvOrFile(
replicaConnections = "POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS"
process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map( );
(conn) => ({ if (replicaConnectionStrings) {
connection_string: conn.trim() replicaConnections = replicaConnectionStrings
}) .split(",")
); .map((conn) => ({
connection_string: conn.trim()
}));
} }
// If no logs database is configured, fall back to main database // If no logs database is configured, fall back to main database
+2 -1
View File
@@ -6,6 +6,7 @@ import fs from "fs";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import logger from "@server/logger"; import logger from "@server/logger";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
export const location = path.join(APP_PATH, "db", "db.sqlite"); export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = checkFileExists(location); export const exists = checkFileExists(location);
@@ -19,7 +20,7 @@ function createDb() {
: undefined; : undefined;
const sqlite = new Database(location, { verbose }); const sqlite = new Database(location, { verbose });
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") { if (readEnvOrFile("ENABLE_SQLITE_WAL_MODE") == "true") {
// Enable WAL mode — allows concurrent readers + single writer, preventing // Enable WAL mode — allows concurrent readers + single writer, preventing
// contention across subsystems (verifySession, Traefik, audit, ping). // contention across subsystems (verifySession, Traefik, audit, ping).
// NOTE: journal_mode persists in the DB file once set; unsetting this // NOTE: journal_mode persists in the DB file once set; unsetting this
+6 -6
View File
@@ -1,6 +1,6 @@
export enum LicenseId { export enum LicenseId {
SMALL_LICENSE = "small_license", TIER1 = "tier1",
BIG_LICENSE = "big_license" TIER2 = "tier2"
} }
export type LicensePriceSet = { export type LicensePriceSet = {
@@ -9,15 +9,15 @@ export type LicensePriceSet = {
export const licensePriceSet: LicensePriceSet = { export const licensePriceSet: LicensePriceSet = {
// Free license matches the freeLimitSet // Free license matches the freeLimitSet
[LicenseId.SMALL_LICENSE]: "price_1TMJzmD3Ee2Ir7Wm05NlGImT", [LicenseId.TIER1]: "price_1TMJzmD3Ee2Ir7Wm05NlGImT",
[LicenseId.BIG_LICENSE]: "price_1TMJzzD3Ee2Ir7WmzJw9TerS" [LicenseId.TIER2]: "price_1TMJzzD3Ee2Ir7WmzJw9TerS"
}; };
export const licensePriceSetSandbox: LicensePriceSet = { export const licensePriceSetSandbox: LicensePriceSet = {
// Free license matches the freeLimitSet // Free license matches the freeLimitSet
// when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value // when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value
[LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", [LicenseId.TIER1]: "price_1SxDwuDCpkOb237Bz0yTiOgN",
[LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" [LicenseId.TIER2]: "price_1SxDy0DCpkOb237BWJxrxYkl"
}; };
export function getLicensePriceSet( export function getLicensePriceSet(
+36 -2
View File
@@ -1,3 +1,37 @@
export const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { import fs from "fs";
return process.env[envVar] ?? valFromYaml;
// Resolves an environment variable, also honoring a `<envVar>_FILE` variant
// that points to a file whose (trimmed) contents should be used as the
// value. This is the common convention for consuming Docker/Swarm secrets
// (e.g. mounted at /run/secrets/...) without putting the raw value in the
// container's environment.
export const readEnvOrFile = (envVar: string): string | undefined => {
const fileEnvVar = `${envVar}_FILE`;
const filePath = process.env[fileEnvVar];
if (filePath) {
if (process.env[envVar]) {
throw new Error(
`Both ${envVar} and ${fileEnvVar} are set. Please set only one.`
);
}
try {
return fs.readFileSync(filePath, "utf8").trim();
} catch (error) {
throw new Error(
`Failed to read ${fileEnvVar} (${filePath}): ${
error instanceof Error ? error.message : error
}`
);
}
}
return process.env[envVar];
}; };
export const getEnvOrYaml =
(envVar: string) =>
(valFromYaml: string | undefined): string | undefined => {
return readEnvOrFile(envVar) ?? valFromYaml;
};
+15 -28
View File
@@ -3,7 +3,7 @@ import * as yaml from "js-yaml";
import { configFilePath1, configFilePath2 } from "./consts"; import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod"; import { z } from "zod";
import stoi from "./stoi"; import stoi from "./stoi";
import { getEnvOrYaml } from "./getEnvOrYaml"; import { getEnvOrYaml, readEnvOrFile } from "./getEnvOrYaml";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
@@ -26,14 +26,12 @@ export const configSchema = z
.object({ .object({
anonymous_usage: z.boolean().optional().default(true) anonymous_usage: z.boolean().optional().default(true)
}) })
.optional()
.prefault({}), .prefault({}),
notifications: z notifications: z
.object({ .object({
product_updates: z.boolean().optional().default(true), product_updates: z.boolean().optional().default(true),
new_releases: z.boolean().optional().default(true) new_releases: z.boolean().optional().default(true)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional() .optional()
@@ -109,7 +107,6 @@ export const configSchema = z
id: z.string().optional().default("P-Access-Token-Id"), id: z.string().optional().default("P-Access-Token-Id"),
token: z.string().optional().default("P-Access-Token") token: z.string().optional().default("P-Access-Token")
}) })
.optional()
.prefault({}), .prefault({}),
remote_headers: z remote_headers: z
.object({ .object({
@@ -126,7 +123,6 @@ export const configSchema = z
name: z.string().optional().default("Remote-Name"), name: z.string().optional().default("Remote-Name"),
role: z.string().optional().default("Remote-Role") role: z.string().optional().default("Remote-Role")
}) })
.optional()
.prefault({}), .prefault({}),
resource_session_request_param: z resource_session_request_param: z
.string() .string()
@@ -164,14 +160,17 @@ export const configSchema = z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.transform((val) => .transform((val) => {
process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER !== const envVal = readEnvOrFile(
undefined "ENABLE_AI_GATEWAY_CLIENT_IP_HEADER"
? process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER === );
"true" return envVal !== undefined ? envVal === "true" : val;
: val }),
), secret: z
secret: z.string().pipe(z.string().min(8)).optional(), .string()
.pipe(z.string().min(8))
.optional()
.transform(getEnvOrYaml("SERVER_SECRET")),
maxmind_db_path: z.string().optional(), maxmind_db_path: z.string().optional(),
maxmind_asn_path: z.string().optional() maxmind_asn_path: z.string().optional()
}) })
@@ -202,7 +201,8 @@ export const configSchema = z
dashboard_session_length_hours: 720, dashboard_session_length_hours: 720,
resource_session_length_hours: 720, resource_session_length_hours: 720,
trust_proxy: 1, trust_proxy: 1,
enable_ai_gateway_client_ip_header: false enable_ai_gateway_client_ip_header: false,
secret: undefined
}), }),
postgres: z postgres: z
.object({ .object({
@@ -238,7 +238,6 @@ export const configSchema = z
.default(5000), .default(5000),
jit_mode: z.boolean().default(true) jit_mode: z.boolean().default(true)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional(), .optional(),
@@ -278,7 +277,6 @@ export const configSchema = z
.optional() .optional()
.default(5000) .default(5000)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional(), .optional(),
@@ -325,10 +323,8 @@ export const configSchema = z
.optional() .optional()
.default(50) .default(50)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional()
.prefault({}), .prefault({}),
gerbil: z gerbil: z
.object({ .object({
@@ -357,7 +353,6 @@ export const configSchema = z
.optional() .optional()
.default(30) .default(30)
}) })
.optional()
.prefault({}), .prefault({}),
orgs: z orgs: z
.object({ .object({
@@ -391,7 +386,6 @@ export const configSchema = z
.optional() .optional()
.default(500) .default(500)
}) })
.optional()
.prefault({}), .prefault({}),
auth: z auth: z
.object({ .object({
@@ -408,10 +402,8 @@ export const configSchema = z
.optional() .optional()
.default(500) .default(500)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional()
.prefault({}), .prefault({}),
email: z email: z
.object({ .object({
@@ -489,10 +481,8 @@ export const configSchema = z
.optional() .optional()
.default(12) .default(12)
}) })
.optional()
.prefault({}) .prefault({})
}) })
.optional()
.prefault({}) .prefault({})
}) })
.refine( .refine(
@@ -513,10 +503,7 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env // If hybrid is not defined, server secret must be defined
if (data.server?.secret === undefined) {
data.server.secret = process.env.SERVER_SECRET;
}
return ( return (
data.server?.secret !== undefined && data.server?.secret !== undefined &&
data.server.secret.length > 0 data.server.secret.length > 0
+53 -24
View File
@@ -14,6 +14,7 @@ import { getTraefikConfig } from "#dynamic/lib/traefik";
import { getValidCertificatesForDomains } from "@server/lib/certificates"; import { getValidCertificatesForDomains } from "@server/lib/certificates";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
import license from "#dynamic/license/license";
export class TraefikConfigManager { export class TraefikConfigManager {
private intervalId: NodeJS.Timeout | null = null; private intervalId: NodeJS.Timeout | null = null;
@@ -357,7 +358,11 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains); this.lastActiveDomains = new Set(domains);
} }
if (process.env.CERT_MODE === "pangolin" && build != "oss") { if (
process.env.CERT_MODE === "pangolin" &&
build != "oss" &&
(await license.hasTier(["personal", "tier2", "enterprise"]))
) {
// Scan current local certificate state // Scan current local certificate state
this.lastLocalCertificateState = this.lastLocalCertificateState =
await this.scanLocalCertificateState(); await this.scanLocalCertificateState();
@@ -717,10 +722,9 @@ export class TraefikConfigManager {
} }
if (shouldWrite) { if (shouldWrite) {
try { try {
fs.writeFileSync( this.atomicWriteFileSync(
traefikDynamicConfigPath, traefikDynamicConfigPath,
yaml.dump(traefikConfig, { noRefs: true }), yaml.dump(traefikConfig, { noRefs: true })
"utf8"
); );
logger.info("Traefik dynamic config updated"); logger.info("Traefik dynamic config updated");
} catch (err) { } catch (err) {
@@ -822,7 +826,7 @@ export class TraefikConfigManager {
// Only write the config if it has changed // Only write the config if it has changed
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
if (newConfigYaml !== originalConfigYaml) { if (newConfigYaml !== originalConfigYaml) {
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); this.atomicWriteFileSync(dynamicConfigPath, newConfigYaml);
logger.info("Dynamic cert config updated from local certificates"); logger.info("Dynamic cert config updated from local certificates");
} }
} }
@@ -900,26 +904,23 @@ export class TraefikConfigManager {
`Processing certificate for domain: ${cert.domain}` `Processing certificate for domain: ${cert.domain}`
); );
fs.writeFileSync(certPath, cert.certFile, "utf8"); // Write atomically (temp file + rename) so Traefik's
fs.writeFileSync(keyPath, cert.keyFile, "utf8"); // file watcher never observes a partially written
// cert/key and fails with "failed to find any PEM data".
// Set appropriate permissions (readable by owner only for key file) this.atomicWriteFileSync(certPath, cert.certFile, 0o644);
fs.chmodSync(certPath, 0o644); this.atomicWriteFileSync(keyPath, cert.keyFile, 0o600);
fs.chmodSync(keyPath, 0o600);
// Write/update .last_update file with current timestamp // Write/update .last_update file with current timestamp
fs.writeFileSync( this.atomicWriteFileSync(
lastUpdatePath, lastUpdatePath,
new Date().toISOString(), new Date().toISOString()
"utf8"
); );
// Check if this is a wildcard certificate and store it // Check if this is a wildcard certificate and store it
const wildcardPath = path.join(domainDir, ".wildcard"); const wildcardPath = path.join(domainDir, ".wildcard");
fs.writeFileSync( this.atomicWriteFileSync(
wildcardPath, wildcardPath,
cert.wildcard ? "true" : "false", cert.wildcard ? "true" : "false"
"utf8"
); );
logger.info( logger.info(
@@ -931,10 +932,9 @@ export class TraefikConfigManager {
// even if the cert content didn't change // even if the cert content didn't change
if (cert.expiresAt) { if (cert.expiresAt) {
const expiresAtPath = path.join(domainDir, ".expires_at"); const expiresAtPath = path.join(domainDir, ".expires_at");
fs.writeFileSync( this.atomicWriteFileSync(
expiresAtPath, expiresAtPath,
cert.expiresAt.toString(), cert.expiresAt.toString()
"utf8"
); );
} }
@@ -970,7 +970,7 @@ export class TraefikConfigManager {
// Only write the config if it has changed // Only write the config if it has changed
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
if (newConfigYaml !== originalConfigYaml) { if (newConfigYaml !== originalConfigYaml) {
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); this.atomicWriteFileSync(dynamicConfigPath, newConfigYaml);
logger.info("Dynamic cert config updated"); logger.info("Dynamic cert config updated");
} }
} }
@@ -1141,10 +1141,9 @@ export class TraefikConfigManager {
if (configChanged) { if (configChanged) {
try { try {
fs.writeFileSync( this.atomicWriteFileSync(
dynamicConfigPath, dynamicConfigPath,
yaml.dump(dynamicConfig, { noRefs: true }), yaml.dump(dynamicConfig, { noRefs: true })
"utf8"
); );
logger.info("Dynamic config updated after cleanup"); logger.info("Dynamic config updated after cleanup");
} catch (err) { } catch (err) {
@@ -1171,6 +1170,36 @@ export class TraefikConfigManager {
} }
} }
/**
* Write a file atomically by writing to a temp file in the same
* directory and renaming it into place. This avoids Traefik (which
* watches these files/directories) picking up a partially written
* file and failing to parse it (e.g. "failed to find any PEM data").
*/
private atomicWriteFileSync(
filePath: string,
data: string,
mode?: number
): void {
const dir = path.dirname(filePath);
const tmpPath = path.join(
dir,
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
try {
fs.writeFileSync(tmpPath, data, "utf8");
if (mode !== undefined) {
fs.chmodSync(tmpPath, mode);
}
fs.renameSync(tmpPath, filePath);
} catch (error) {
try {
fs.rmSync(tmpPath, { force: true });
} catch {}
throw error;
}
}
/** /**
* Check if file exists * Check if file exists
*/ */
+6 -2
View File
@@ -4,7 +4,7 @@ import { setHostMeta } from "@server/lib/hostMeta";
const keyTypes = ["host"] as const; const keyTypes = ["host"] as const;
export type LicenseKeyType = (typeof keyTypes)[number]; export type LicenseKeyType = (typeof keyTypes)[number];
const keyTiers = ["personal", "enterprise"] as const; const keyTiers = ["personal", "enterprise", "tier1", "tier2"] as const;
export type LicenseKeyTier = (typeof keyTiers)[number]; export type LicenseKeyTier = (typeof keyTiers)[number];
export type LicenseStatus = { export type LicenseStatus = {
@@ -33,7 +33,7 @@ export type LicenseKeyCache = {
export class License { export class License {
private serverSecret!: string; private serverSecret!: string;
constructor(private hostMeta: HostMeta) { } constructor(private hostMeta: HostMeta) {}
public async check(): Promise<LicenseStatus> { public async check(): Promise<LicenseStatus> {
return { return {
@@ -50,6 +50,10 @@ export class License {
public async isUnlocked() { public async isUnlocked() {
return false; return false;
} }
public async hasTier(tier: LicenseKeyTier[]) {
return false;
}
} }
await setHostMeta(); await setHostMeta();
+2 -2
View File
@@ -45,9 +45,9 @@ export class JobScheduler {
label: string label: string
): () => Promise<void> { ): () => Promise<void> {
return async () => { return async () => {
if (!(await license.isUnlocked())) { if (!(await license.hasTier(["personal", "tier2", "enterprise"]))) {
logger.debug( logger.debug(
`Skipping ${label} tick - license is not subscribed` `Skipping ${label} tick - requires a tier2 license`
); );
return; return;
} }
+11 -6
View File
@@ -63,10 +63,11 @@ export class AuthoritativeDNSServer {
private allDomains: Set<string> = new Set(); private allDomains: Set<string> = new Set();
private domainRefreshInterval: NodeJS.Timeout | null = null; private domainRefreshInterval: NodeJS.Timeout | null = null;
// Cached license/subscription status. license.isUnlocked() does a DB // Cached license/plan status - only a tier2 license unlocks the DNS
// round-trip on every call, so it can't be checked per-query on a UDP // server. license.hasPlan() does a DB round-trip on every call, so it
// server that may see very high query volume - instead it's polled on // can't be checked per-query on a UDP server that may see very high
// the same cadence as the domain set refresh and read from memory here. // query volume - instead it's polled on the same cadence as the domain
// set refresh and read from memory here.
private isLicensed: boolean = false; private isLicensed: boolean = false;
private licenseRefreshInterval: NodeJS.Timeout | null = null; private licenseRefreshInterval: NodeJS.Timeout | null = null;
@@ -128,7 +129,7 @@ export class AuthoritativeDNSServer {
} }
if (!this.isLicensed) { if (!this.isLicensed) {
logger.debug("Refusing DNS query - license is not subscribed"); logger.debug("Refusing DNS query - requires a tier2 license");
// REFUSED (rcode=5) indicates a policy refusal by this nameserver. // REFUSED (rcode=5) indicates a policy refusal by this nameserver.
this.sendResponse(packet, [], rinfo, false, 5, []); this.sendResponse(packet, [], rinfo, false, 5, []);
return; return;
@@ -1042,7 +1043,11 @@ export class AuthoritativeDNSServer {
private async refreshLicenseStatus(): Promise<void> { private async refreshLicenseStatus(): Promise<void> {
try { try {
this.isLicensed = await license.isUnlocked(); this.isLicensed = await license.hasTier([
"personal",
"tier2",
"enterprise"
]);
} catch (error) { } catch (error) {
logger.error("Failed to refresh license status:", error); logger.error("Failed to refresh license status:", error);
this.isLicensed = false; this.isLicensed = false;
-3
View File
@@ -48,7 +48,6 @@ export const privateConfigSchema = z
.optional() .optional()
.transform(getEnvOrYaml("FOSSORIAL_API_KEY")) .transform(getEnvOrYaml("FOSSORIAL_API_KEY"))
}) })
.optional()
.prefault({}), .prefault({}),
redis: z redis: z
.object({ .object({
@@ -166,7 +165,6 @@ export const privateConfigSchema = z
.optional() .optional()
.default("http://gerbil:3004") .default("http://gerbil:3004")
}) })
.optional()
.prefault({}), .prefault({}),
flags: z flags: z
.object({ .object({
@@ -187,7 +185,6 @@ export const privateConfigSchema = z
// (server/private/lib/config.ts). // (server/private/lib/config.ts).
disable_private_http_placeholder: z.boolean().optional() disable_private_http_placeholder: z.boolean().optional()
}) })
.optional()
.prefault({}), .prefault({}),
acme: z acme: z
.object({ .object({
+40 -51
View File
@@ -54,6 +54,7 @@ import {
getValidCertificatesForDomains getValidCertificatesForDomains
} from "@server/lib/certificates"; } from "@server/lib/certificates";
import { build } from "@server/build"; import { build } from "@server/build";
import license from "#private/license/license";
import regionalCache from "#private/lib/cache"; import regionalCache from "#private/lib/cache";
import { TargetWithSite } from "@server/lib/traefik/types"; import { TargetWithSite } from "@server/lib/traefik/types";
import { buildWildcardTls } from "@server/lib/traefik/certResolver"; import { buildWildcardTls } from "@server/lib/traefik/certResolver";
@@ -395,8 +396,15 @@ export async function getTraefikConfig(
) )
); );
// Pangolin-managed DNS-01/ACME cert mode requires either a tier1
// license (self-hosted) or a saas build - otherwise fall back to
// Traefik's own cert resolvers (buildWildcardTls) throughout.
const pangolinCertModeEnabled =
privateConfig.getRawPrivateConfig().acme?.cert_mode == "pangolin" &&
(await license.hasTier(["personal", "tier2", "enterprise"]));
let validCerts: CertificateResult[] = []; let validCerts: CertificateResult[] = [];
if (privateConfig.getRawPrivateConfig().acme?.cert_mode == "pangolin") { if (pangolinCertModeEnabled) {
// create a list of all domains to get certs for // create a list of all domains to get certs for
const domains = new Set<string>(); const domains = new Set<string>();
for (const resource of resourcesMap.values()) { for (const resource of resourcesMap.values()) {
@@ -522,10 +530,7 @@ export async function getTraefikConfig(
); );
let tls = {}; let tls = {};
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
tls = buildWildcardTls({ tls = buildWildcardTls({
fullDomain, fullDomain,
hasSubdomain: !!resource.subdomain, hasSubdomain: !!resource.subdomain,
@@ -546,6 +551,30 @@ export async function getTraefikConfig(
} }
} }
config_output.http.services![serviceName] = {
loadBalancer: {
servers: buildHttpLoadBalancerServers(targets),
...(resource.stickySession
? buildStickySessionCookie(resource.ssl)
: {})
}
};
if (resource.tlsServerName) {
if (!config_output.http.serversTransports) {
config_output.http.serversTransports = {};
}
config_output.http.serversTransports![transportName] = {
serverName: resource.tlsServerName,
//unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings
// if defined in the static config and here. if not set, self-signed certs won't work
insecureSkipVerify: true
};
config_output.http.services![
serviceName
].loadBalancer.serversTransport = transportName;
}
if (resource.ssl) { if (resource.ssl) {
config_output.http.routers![routerName + "-redirect"] = { config_output.http.routers![routerName + "-redirect"] = {
entryPoints: [ entryPoints: [
@@ -710,31 +739,6 @@ export async function getTraefikConfig(
priority: priority, priority: priority,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
}; };
config_output.http.services![serviceName] = {
loadBalancer: {
servers: buildHttpLoadBalancerServers(targets),
...(resource.stickySession
? buildStickySessionCookie(resource.ssl)
: {})
}
};
// Add the serversTransport if TLS server name is provided
if (resource.tlsServerName) {
if (!config_output.http.serversTransports) {
config_output.http.serversTransports = {};
}
config_output.http.serversTransports![transportName] = {
serverName: resource.tlsServerName,
//unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings
// if defined in the static config and here. if not set, self-signed certs won't work
insecureSkipVerify: true
};
config_output.http.services![
serviceName
].loadBalancer.serversTransport = transportName;
}
} else if (resource.mode == "tcp" || resource.mode == "udp") { } else if (resource.mode == "tcp" || resource.mode == "udp") {
// Non-HTTP (TCP/UDP) configuration // Non-HTTP (TCP/UDP) configuration
if (!resource.enableProxy) { if (!resource.enableProxy) {
@@ -791,10 +795,7 @@ export async function getTraefikConfig(
domainCertResolver, domainCertResolver,
preferWildcardCert preferWildcardCert
}) => { }) => {
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
return buildWildcardTls({ return buildWildcardTls({
fullDomain, fullDomain,
hasSubdomain, hasSubdomain,
@@ -835,10 +836,7 @@ export async function getTraefikConfig(
maintenancePageUiUrl, maintenancePageUiUrl,
redirectHttpsMiddlewareName, redirectHttpsMiddlewareName,
resolveTls: (fullDomain) => { resolveTls: (fullDomain) => {
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
// siteResource aliases don't have a per-domain cert // siteResource aliases don't have a per-domain cert
// resolver stored, so always fall back to the global // resolver stored, so always fall back to the global
// defaults. // defaults.
@@ -929,10 +927,7 @@ export async function getTraefikConfig(
const rule = buildHostRule(fullDomain, ir.wildcard); const rule = buildHostRule(fullDomain, ir.wildcard);
let tls: any = {}; let tls: any = {};
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
tls = buildWildcardTls({ tls = buildWildcardTls({
fullDomain, fullDomain,
hasSubdomain: !!ir.subdomain, hasSubdomain: !!ir.subdomain,
@@ -1012,10 +1007,7 @@ export async function getTraefikConfig(
const rule = `Host(\`${fullDomain}\`) && ClientIP(\`${exitNode.address}\`)`; // restrict to coming from the exit node ip range that the client is connected to const rule = `Host(\`${fullDomain}\`) && ClientIP(\`${exitNode.address}\`)`; // restrict to coming from the exit node ip range that the client is connected to
let tls: any = {}; let tls: any = {};
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
// siteResource aliases don't have a per-domain cert // siteResource aliases don't have a per-domain cert
// resolver stored, so always fall back to the global // resolver stored, so always fall back to the global
// defaults. // defaults.
@@ -1089,7 +1081,7 @@ export async function getTraefikConfig(
.where(eq(exitNodes.exitNodeId, exitNodeId)); .where(eq(exitNodes.exitNodeId, exitNodeId));
let validCertsLoginPages: CertificateResult[] = []; let validCertsLoginPages: CertificateResult[] = [];
if (privateConfig.getRawPrivateConfig().acme?.cert_mode == "pangolin") { if (pangolinCertModeEnabled) {
// create a list of all domains to get certs for // create a list of all domains to get certs for
const domains = new Set<string>(); const domains = new Set<string>();
for (const lp of exitNodeLoginPages) { for (const lp of exitNodeLoginPages) {
@@ -1134,10 +1126,7 @@ export async function getTraefikConfig(
} }
const tls = {}; const tls = {};
if ( if (!pangolinCertModeEnabled) {
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
"pangolin"
) {
// TODO: we need to add the wildcard logic here too // TODO: we need to add the wildcard logic here too
} else { } else {
// find a cert that matches the full domain, if not continue // find a cert that matches the full domain, if not continue
+19 -2
View File
@@ -26,6 +26,7 @@ import {
LicenseStatus LicenseStatus
} from "@server/license/license"; } from "@server/license/license";
import { setHostMeta } from "@server/lib/hostMeta"; import { setHostMeta } from "@server/lib/hostMeta";
import { build } from "@server/build";
type ActivateLicenseKeyAPIResponse = { type ActivateLicenseKeyAPIResponse = {
data: { data: {
@@ -119,6 +120,9 @@ LQIDAQAB
} }
public async isUnlocked(): Promise<boolean> { public async isUnlocked(): Promise<boolean> {
if (build == "saas") {
return true;
}
const status = await this.check(); const status = await this.check();
if (status.isHostLicensed) { if (status.isHostLicensed) {
if (status.isLicenseValid) { if (status.isLicenseValid) {
@@ -128,6 +132,20 @@ LQIDAQAB
return false; return false;
} }
public async hasTier(tier: LicenseKeyTier[]): Promise<boolean> {
if (build == "saas") {
return true;
}
const status = await this.check();
if (status.isHostLicensed && status.isLicenseValid) {
return (
status.tier !== undefined &&
tier.includes(status.tier as LicenseKeyTier)
);
}
return false;
}
public async check(): Promise<LicenseStatus> { public async check(): Promise<LicenseStatus> {
// If a check is already in progress, return the last known status // If a check is already in progress, return the last known status
if (this.checkInProgress) { if (this.checkInProgress) {
@@ -135,8 +153,7 @@ LQIDAQAB
"License check already in progress, returning last known status" "License check already in progress, returning last known status"
); );
const lastStatus = this.statusCache.get(this.statusKey) as const lastStatus = this.statusCache.get(this.statusKey) as
| LicenseStatus LicenseStatus | undefined;
| undefined;
if (lastStatus) { if (lastStatus) {
return lastStatus; return lastStatus;
} }
@@ -54,7 +54,6 @@ export const queryAccessAuditLogsQuery = z.object({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -47,7 +47,6 @@ export const queryActionAuditLogsQuery = z.object({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -55,7 +55,6 @@ export const queryConnectionAuditLogsQuery = z.object({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -222,15 +222,16 @@ export async function handleSubscriptionCreated(
let numUsers: number; let numUsers: number;
let numSites: number; let numSites: number;
let tier = "enterprise";
if (subscriptionPriceId === priceSet[LicenseId.SMALL_LICENSE]) { if (subscriptionPriceId === priceSet[LicenseId.TIER1]) {
numUsers = 25; numUsers = 25;
numSites = 25; numSites = 25;
} else if ( tier = "tier1";
subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] } else if (subscriptionPriceId === priceSet[LicenseId.TIER2]) {
) {
numUsers = 50; numUsers = 50;
numSites = 100; numSites = 100;
tier = "tier2";
} else { } else {
logger.error( logger.error(
`Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}`
@@ -256,7 +257,8 @@ export async function handleSubscriptionCreated(
licenseId: parseInt(licenseId), licenseId: parseInt(licenseId),
paidFor: true, paidFor: true,
users: numUsers, users: numUsers,
sites: numSites sites: numSites,
tier: tier
}) })
} }
); );
@@ -64,14 +64,11 @@ export async function generateNewEnterpriseLicense(
const licenseData = req.body; const licenseData = req.body;
if ( if (licenseData.tier != "tier2" && licenseData.tier != "tier1") {
licenseData.tier != "big_license" &&
licenseData.tier != "small_license"
) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Invalid tier specified. Must be either 'big_license' or 'small_license'." "Invalid tier specified. Must be either 'tier2' or 'tier1'."
) )
); );
} }
@@ -118,9 +115,7 @@ export async function generateNewEnterpriseLicense(
} }
const tier = const tier =
licenseData.tier === "big_license" licenseData.tier === "tier2" ? LicenseId.TIER2 : LicenseId.TIER1;
? LicenseId.BIG_LICENSE
: LicenseId.SMALL_LICENSE;
const tierPrice = getLicensePriceSet()[tier]; const tierPrice = getLicensePriceSet()[tier];
const session = await stripe!.checkout.sessions.create({ const session = await stripe!.checkout.sessions.create({
@@ -47,7 +47,6 @@ export const queryAiSessionLogsQuery = z.strictObject({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -20,7 +20,6 @@ const queryAccessAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => getSevenDaysAgo().toISOString()) .prefault(() => getSevenDaysAgo().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -34,7 +33,6 @@ const queryAccessAuditLogsQuery = z.object({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
@@ -41,7 +41,6 @@ export const queryAccessAuditLogsQuery = z.strictObject({
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString()) .prefault(() => new Date().toISOString())
.openapi({ .openapi({
type: "string", type: "string",
+4 -4
View File
@@ -17,7 +17,7 @@ import {
import type { import type {
GenerateRegistrationOptionsOpts, GenerateRegistrationOptionsOpts,
GenerateAuthenticationOptionsOpts, GenerateAuthenticationOptionsOpts,
AuthenticatorTransportFuture AuthenticatorTransport
} from "@simplewebauthn/server"; } from "@simplewebauthn/server";
import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { isoBase64URL } from "@simplewebauthn/server/helpers";
import config from "@server/lib/config"; import config from "@server/lib/config";
@@ -221,7 +221,7 @@ export async function startRegistration(
const excludeCredentials = existingSecurityKeys.map((key) => ({ const excludeCredentials = existingSecurityKeys.map((key) => ({
id: key.credentialId, id: key.credentialId,
transports: key.transports transports: key.transports
? (JSON.parse(key.transports) as AuthenticatorTransportFuture[]) ? (JSON.parse(key.transports) as AuthenticatorTransport[])
: undefined : undefined
})); }));
@@ -571,7 +571,7 @@ export async function startAuthentication(
transports: key.transports transports: key.transports
? (JSON.parse( ? (JSON.parse(
key.transports key.transports
) as AuthenticatorTransportFuture[]) ) as AuthenticatorTransport[])
: undefined : undefined
})); }));
} }
@@ -702,7 +702,7 @@ export async function verifyAuthentication(
transports: securityKey.transports transports: securityKey.transports
? (JSON.parse( ? (JSON.parse(
securityKey.transports securityKey.transports
) as AuthenticatorTransportFuture[]) ) as AuthenticatorTransport[])
: undefined : undefined
}, },
requireUserVerification: false requireUserVerification: false
@@ -43,7 +43,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
const { message, client: c } = context; const { message, client: c } = context;
const newt = c as Newt; const newt = c as Newt;
logger.info("Handling healthcheck status message"); logger.debug("Handling healthcheck status message");
if (!newt) { if (!newt) {
logger.warn("Newt not found"); logger.warn("Newt not found");
+14 -13
View File
@@ -252,9 +252,10 @@ export default function GenerateLicenseKeyForm({
try { try {
// Check if this is a business/enterprise license request // Check if this is a business/enterprise license request
if (payload.useCaseType === "business") { if (payload.useCaseType === "business") {
const response = await api.put< const response = await api.put<AxiosResponse<string>>(
AxiosResponse<string> `/org/${orgId}/license/enterprise`,
>(`/org/${orgId}/license/enterprise`, { ...payload, tier: "big_license" } ); { ...payload, tier: "tier2" }
);
console.log("Checkout session response:", response.data); console.log("Checkout session response:", response.data);
const checkoutUrl = response.data.data; const checkoutUrl = response.data.data;
@@ -1087,16 +1088,16 @@ export default function GenerateLicenseKeyForm({
)} )}
{!generatedKey && useCaseType === "business" && ( {!generatedKey && useCaseType === "business" && (
<Button <Button
type="submit" type="submit"
form="generate-license-business-form" form="generate-license-business-form"
disabled={loading} disabled={loading}
loading={loading} loading={loading}
> >
{t( {t(
"generateLicenseKeyForm.buttons.generateLicenseKey" "generateLicenseKeyForm.buttons.generateLicenseKey"
)} )}
</Button> </Button>
)} )}
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
+10 -3
View File
@@ -201,9 +201,16 @@ export default function GenerateLicenseKeysTable({
}, },
cell: ({ row }) => { cell: ({ row }) => {
const tier = row.original.tier; const tier = row.original.tier;
return tier === "enterprise" switch (tier) {
? t("licenseTierEnterprise") case "enterprise":
: t("licenseTierPersonal"); return t("licenseTierEnterprise");
case "tier1":
return t("licenseTierTier1");
case "tier2":
return t("licenseTierTier2");
default:
return t("licenseTierPersonal");
}
} }
}, },
{ {
+10 -3
View File
@@ -100,9 +100,16 @@ export function LicenseKeysDataTable({
}, },
cell: ({ row }) => { cell: ({ row }) => {
const tier = row.original.tier; const tier = row.original.tier;
return tier === "enterprise" switch (tier) {
? t("licenseTierEnterprise") case "enterprise":
: t("licenseTierPersonal"); return t("licenseTierEnterprise");
case "tier1":
return t("licenseTierTier1");
case "tier2":
return t("licenseTierTier2");
default:
return t("licenseTierPersonal");
}
} }
}, },
{ {
+2 -2
View File
@@ -40,8 +40,8 @@ import { InfoIcon } from "lucide-react";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
const TIER_TO_LICENSE_ID = { const TIER_TO_LICENSE_ID = {
starter: "small_license", starter: "tier1",
scale: "big_license" scale: "tier2"
} as const; } as const;
type FormProps = { type FormProps = {
+1 -1
View File
@@ -87,7 +87,7 @@ function Calendar({
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label defaultClassNames.caption_label
), ),
table: "w-full border-collapse", month_grid: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",