Compare commits

..

1 Commits

Author SHA1 Message Date
Owen Schwartz cf596d980f Merge pull request #2971 from fosrl/dev
1.18.2
2026-05-02 20:59:51 -07:00
35 changed files with 114 additions and 1663 deletions
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Превишихте ограничението на текущия си план. Коригирайте проблема, като премахнете сайтове, потребители или други ресурси, за да оставате в рамките на плана си.",
"trialBannerMessage": "Пробният Ви период изтича след {countdown}. Актуализирайте за запазване на достъпа.",
"trialBannerExpired": "Пробният Ви период е изтекъл. Актуализирайте сега, за да възстановите достъпа.",
"billingTrialBannerTitle": "Пробният период е активен",
"billingTrialBannerDescription": "В момента сте в пробен период на бизнес ниво. След края на пробния период, вашият акаунт автоматично ще бъде върнат към функциите и ограниченията на основното ниво. Надградете по всяко време, за да запазите достъпа до текущите функции на плана.",
"billingTrialBannerUpgrade": "Надградете сега",
"billingTrialBadge": "Пробен период",
"trialActive": "Активен пробен период",
"trialExpired": "Пробният период е изтекъл",
"trialHasEnded": "Пробният Ви период е приключил.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
"domainPickerWildcardCertWarningLink": "Научете повече",
"health": "Здраве",
"domainPendingErrorTitle": "Проблем при проверка",
"memberPortalTitle": "Ресурси",
"memberPortalDescription": "Ресурси, до които имате достъп в тази организация",
"memberPortalSortBy": "Сортиране по...",
"memberPortalSortNameAsc": "Име А-Я",
"memberPortalSortNameDesc": "Име Я-А",
"memberPortalSortDomainAsc": "Домен А-Я",
"memberPortalSortDomainDesc": "Домен Я-А",
"memberPortalSortEnabledFirst": "Активирани Първи",
"memberPortalSortDisabledFirst": "Деактивирани Първи",
"memberPortalRefresh": "Обнови",
"memberPortalRefreshResources": "Обнови ресурсите",
"memberPortalFailedToLoad": "Грешка при зареждане на ресурсите",
"memberPortalFailedToLoadDescription": "Грешка при зареждане на ресурсите. Моля, проверете връзката си и опитайте отново.",
"memberPortalUnableToLoad": "Неуспешно зареждане на ресурси",
"memberPortalTryAgain": "Опитай отново",
"memberPortalNoResourcesFound": "Няма намерени ресурси",
"memberPortalNoResourcesAvailable": "Няма налични ресурси",
"memberPortalNoResourcesMatchSearch": "Няма ресурси, съвпадащи с \"{query}\". Опитайте да промените търсените условия или нулирайте търсенето, за да видите всички ресурси.",
"memberPortalNoResourcesAccess": "Още нямате достъп до ресурси. Свържете се с вашия администратор, за да получите достъп до нужните ресурси.",
"memberPortalClearSearch": "Изчисти търсенето",
"memberPortalPublicResources": "Публични ресурси",
"memberPortalPublicResourcesDescription": "Уеб приложения и услуги, достъпни през браузър",
"memberPortalCopiedToClipboard": "Копирано в клипборда",
"memberPortalCopiedUrlDescription": "URL адресът на ресурса е копиран в клипборда.",
"memberPortalOpenResource": "Отвори ресурса",
"memberPortalPrivateResources": "Частни ресурси",
"memberPortalPrivateResourcesDescription": "Ресурси на вътрешната мрежа, достъпни чрез клиент",
"memberPortalResourceDetails": "Детайли за ресурса",
"memberPortalMode": "Режим",
"memberPortalDestination": "Дестинация",
"memberPortalAlias": "Алиас",
"memberPortalCopiedAliasDescription": "Алиасът на ресурса е копиран в клипборда.",
"memberPortalCopiedDestinationDescription": "Дестинацията на ресурса е копирана в клипборда.",
"memberPortalRequiresClientConnection": "Изисква връзка с клиента",
"memberPortalAuthMethods": "Методи на удостоверяване",
"memberPortalSso": "Единно вход (SSO)",
"memberPortalPasswordProtected": "Защитено с парола",
"memberPortalPinCode": "ПИН код",
"memberPortalEmailWhitelist": "Бял списък на имейли",
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
"memberPortalPrevious": "Предишен",
"memberPortalNext": "Следващ"
"domainPendingErrorTitle": "Проблем при проверка"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Jste za hranicemi vašeho aktuálního plánu. Opravte problém odstraněním webů, uživatelů nebo jiných zdrojů, abyste zůstali ve vašem tarifu.",
"trialBannerMessage": "Vaše zkušební verze vyprší za {countdown}. Pro udržení přístupu upgraduje.",
"trialBannerExpired": "Vaše zkušební verze vypršela. Upgradujte nyní pro obnovu přístupu.",
"billingTrialBannerTitle": "Aktivní zkušební verze",
"billingTrialBannerDescription": "Právě používáte zkušební verzi na úrovni business. Po skončení zkušební verze se váš účet automaticky vrátí k funkcím a limitům úrovně Basic. Upgradujte kdykoli pro zachování přístupu k funkcím vašeho aktuálního plánu.",
"billingTrialBannerUpgrade": "Upgradovat nyní",
"billingTrialBadge": "Zkušební verze",
"trialActive": "Zkušební verze je aktivní",
"trialExpired": "Zkušební verze vypršela",
"trialHasEnded": "Vaše zkušební verze skončila.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
"domainPickerWildcardCertWarningLink": "Zjistit více",
"health": "Zdraví",
"domainPendingErrorTitle": "Problém s ověřením",
"memberPortalTitle": "Zdroje",
"memberPortalDescription": "Zdroje, ke kterým máte v této organizaci přístup",
"memberPortalSortBy": "Řadit podle...",
"memberPortalSortNameAsc": "Názvu A-Z",
"memberPortalSortNameDesc": "Názvu Z-A",
"memberPortalSortDomainAsc": "Domény A-Z",
"memberPortalSortDomainDesc": "Domény Z-A",
"memberPortalSortEnabledFirst": "Nejprve povoleno",
"memberPortalSortDisabledFirst": "Nejprve zakázáno",
"memberPortalRefresh": "Aktualizovat",
"memberPortalRefreshResources": "Aktualizovat zdroje",
"memberPortalFailedToLoad": "Nepodařilo se načíst zdroje",
"memberPortalFailedToLoadDescription": "Nepodařilo se načíst zdroje. Zkontrolujte prosím své připojení a zkuste to znovu.",
"memberPortalUnableToLoad": "Nelze načíst zdroje",
"memberPortalTryAgain": "Zkusit znovu",
"memberPortalNoResourcesFound": "Žádné zdroje nebyly nalezeny",
"memberPortalNoResourcesAvailable": "Žádné zdroje nejsou k dispozici",
"memberPortalNoResourcesMatchSearch": "Žádné zdroje neodpovídají \"{query}\". Zkuste přizpůsobit své vyhledávací termíny nebo vyčistit hledání, abyste viděli všechny zdroje.",
"memberPortalNoResourcesAccess": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého správce, aby vám poskytl přístup k potřebným zdrojům.",
"memberPortalClearSearch": "Vymazat hledání",
"memberPortalPublicResources": "Veřejné zdroje",
"memberPortalPublicResourcesDescription": "Webové aplikace a služby přístupné přes prohlížeč",
"memberPortalCopiedToClipboard": "Zkopírováno do schránky",
"memberPortalCopiedUrlDescription": "URL zdroje byla zkopírována do vaší schránky.",
"memberPortalOpenResource": "Otevřít zdroj",
"memberPortalPrivateResources": "Soukromé zdroje",
"memberPortalPrivateResourcesDescription": "Interní síťové zdroje přístupné přes klienta",
"memberPortalResourceDetails": "Podrobnosti o zdroji",
"memberPortalMode": "Režim",
"memberPortalDestination": "Cíl",
"memberPortalAlias": "Přezdívka",
"memberPortalCopiedAliasDescription": "Alias zdroje byl zkopírován do vaší schránky.",
"memberPortalCopiedDestinationDescription": "Cíl zdroje byl zkopírován do vaší schránky.",
"memberPortalRequiresClientConnection": "Vyžaduje klientské připojení",
"memberPortalAuthMethods": "Metody ověřování",
"memberPortalSso": "Jedno přihlášení (SSO)",
"memberPortalPasswordProtected": "Heslo chráněno",
"memberPortalPinCode": "PIN kód",
"memberPortalEmailWhitelist": "Seznam povolených emailů",
"memberPortalResourceDisabled": "Zdroj je zakázán",
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
"memberPortalPrevious": "Předchozí",
"memberPortalNext": "Následující"
"domainPendingErrorTitle": "Problém s ověřením"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Sie überschreiten Ihre Grenzen für Ihr aktuelles Paket. Korrigieren Sie das Problem, indem Sie Webseiten, Benutzer oder andere Ressourcen entfernen, um in Ihrem Paket zu bleiben.",
"trialBannerMessage": "Ihre Testversion läuft in {countdown} ab. Upgraden, um den Zugriff zu behalten.",
"trialBannerExpired": "Ihre Testversion ist abgelaufen. Jetzt upgraden, um den Zugriff wiederherzustellen.",
"billingTrialBannerTitle": "Kostenlose Testversion aktiv",
"billingTrialBannerDescription": "Sie nutzen derzeit eine kostenlose Testversion auf der Geschäftsstufe. Wenn die Testversion endet, wird Ihr Konto automatisch auf die Funktionen und Beschränkungen der Basisstufe zurückgesetzt. Upgraden Sie jederzeit, um weiterhin Zugriff auf die Funktionen Ihres aktuellen Plans zu behalten.",
"billingTrialBannerUpgrade": "Jetzt upgraden",
"billingTrialBadge": "Kostenlose Testversion",
"trialActive": "Kostenlose Testversion aktiv",
"trialExpired": "Testversion abgelaufen",
"trialHasEnded": "Ihre Testversion ist beendet.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
"health": "Gesundheit",
"domainPendingErrorTitle": "Verifizierungsproblem",
"memberPortalTitle": "Ressourcen",
"memberPortalDescription": "Ressourcen, auf die Sie in dieser Organisation Zugriff haben",
"memberPortalSortBy": "Sortieren nach...",
"memberPortalSortNameAsc": "Name A-Z",
"memberPortalSortNameDesc": "Name Z-A",
"memberPortalSortDomainAsc": "Domain A-Z",
"memberPortalSortDomainDesc": "Domain Z-A",
"memberPortalSortEnabledFirst": "Zuerst aktiviert",
"memberPortalSortDisabledFirst": "Zuerst deaktiviert",
"memberPortalRefresh": "Aktualisieren",
"memberPortalRefreshResources": "Ressourcen aktualisieren",
"memberPortalFailedToLoad": "Fehler beim Laden der Ressourcen",
"memberPortalFailedToLoadDescription": "Fehler beim Laden der Ressourcen. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
"memberPortalUnableToLoad": "Ressourcen konnten nicht geladen werden",
"memberPortalTryAgain": "Nochmal versuchen",
"memberPortalNoResourcesFound": "Keine Ressourcen gefunden",
"memberPortalNoResourcesAvailable": "Keine Ressourcen verfügbar",
"memberPortalNoResourcesMatchSearch": "Keine Ressourcen passen zu \"{query}\". Versuchen Sie, Ihre Suchbegriffe anzupassen oder die Suche zu löschen, um alle Ressourcen anzuzeigen.",
"memberPortalNoResourcesAccess": "Sie haben noch keinen Zugriff auf Ressourcen. Wenden Sie sich an Ihren Administrator, um Zugriff auf die benötigten Ressourcen zu erhalten.",
"memberPortalClearSearch": "Suchverlauf löschen",
"memberPortalPublicResources": "Öffentliche Ressourcen",
"memberPortalPublicResourcesDescription": "Webanwendungen und Dienste, die über den Browser zugänglich sind",
"memberPortalCopiedToClipboard": "In die Zwischenablage kopiert",
"memberPortalCopiedUrlDescription": "Ressourcen-URL wurde in Ihre Zwischenablage kopiert.",
"memberPortalOpenResource": "Ressource öffnen",
"memberPortalPrivateResources": "Private Ressourcen",
"memberPortalPrivateResourcesDescription": "Interne Netzwerkressourcen, die über den Client zugänglich sind",
"memberPortalResourceDetails": "Ressourcendetails",
"memberPortalMode": "Modus",
"memberPortalDestination": "Ziel",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "Ressourcenalias wurde in Ihre Zwischenablage kopiert.",
"memberPortalCopiedDestinationDescription": "Ressourcenziel wurde in Ihre Zwischenablage kopiert.",
"memberPortalRequiresClientConnection": "Erfordert Client-Verbindung",
"memberPortalAuthMethods": "Authentifizierungsmethoden",
"memberPortalSso": "Single Sign-On (SSO)",
"memberPortalPasswordProtected": "Passwortgeschützt",
"memberPortalPinCode": "PIN-Code",
"memberPortalEmailWhitelist": "E-Mail-Whitelist",
"memberPortalResourceDisabled": "Ressource deaktiviert",
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
"memberPortalPrevious": "Vorherige",
"memberPortalNext": "Nächste"
"domainPendingErrorTitle": "Verifizierungsproblem"
}
-4
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "You're beyond your limits for your current plan. Correct the problem by removing sites, users, or other resources to stay within your plan.",
"trialBannerMessage": "Your trial expires in {countdown}. Upgrade to keep access.",
"trialBannerExpired": "Your trial has expired. Upgrade now to restore access.",
"billingTrialBannerTitle": "Free Trial Active",
"billingTrialBannerDescription": "You're currently on a free trial on the business tier. When the trial ends, your account will automatically revert to the Basic tier features and limits. Upgrade anytime to keep access to your current plan's features.",
"billingTrialBannerUpgrade": "Upgrade Now",
"billingTrialBadge": "Free Trial",
"trialActive": "Free Trial Active",
"trialExpired": "Trial Expired",
"trialHasEnded": "Your trial has ended.",
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Estás más allá de tus límites para tu plan actual. Corrija el problema eliminando sitios, usuarios u otros recursos para permanecer dentro de tu plan.",
"trialBannerMessage": "Su prueba expira en {countdown}. Actualice para mantener el acceso.",
"trialBannerExpired": "Su prueba ha expirado. Actualice ahora para restaurar el acceso.",
"billingTrialBannerTitle": "Prueba gratuita activada",
"billingTrialBannerDescription": "Actualmente estás en una prueba gratuita en el nivel empresarial. Cuando finalice la prueba, tu cuenta volverá automáticamente a las características y límites del nivel Básico. Mejora en cualquier momento para mantener el acceso a las características de tu plan actual.",
"billingTrialBannerUpgrade": "Actualizar ahora",
"billingTrialBadge": "Prueba Gratuita",
"trialActive": "Prueba gratuita activa",
"trialExpired": "Prueba expirada",
"trialHasEnded": "Su prueba ha terminado.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
"domainPickerWildcardCertWarningLink": "Más información",
"health": "Salud",
"domainPendingErrorTitle": "Problema de verificación",
"memberPortalTitle": "Recursos",
"memberPortalDescription": "Recursos a los que tiene acceso en esta organización",
"memberPortalSortBy": "Ordenar por...",
"memberPortalSortNameAsc": "Nombre A-Z",
"memberPortalSortNameDesc": "Nombre Z-A",
"memberPortalSortDomainAsc": "Dominio A-Z",
"memberPortalSortDomainDesc": "Dominio Z-A",
"memberPortalSortEnabledFirst": "Habilitado Primero",
"memberPortalSortDisabledFirst": "Deshabilitado Primero",
"memberPortalRefresh": "Actualizar",
"memberPortalRefreshResources": "Actualizar Recursos",
"memberPortalFailedToLoad": "No se pudieron cargar los recursos",
"memberPortalFailedToLoadDescription": "No se pudieron cargar los recursos. Por favor, revise su conexión e intente de nuevo.",
"memberPortalUnableToLoad": "No se pudieron cargar los recursos",
"memberPortalTryAgain": "Intentar de Nuevo",
"memberPortalNoResourcesFound": "No se encontraron Recursos",
"memberPortalNoResourcesAvailable": "No Hay Recursos Disponibles",
"memberPortalNoResourcesMatchSearch": "No hay recursos que coincidan con \"{query}\". Intenta ajustar tus términos de búsqueda o limpiar la búsqueda para ver todos los recursos.",
"memberPortalNoResourcesAccess": "Aún no tiene acceso a ningún recurso. Comuníquese con su administrador para obtener acceso a los recursos que necesita.",
"memberPortalClearSearch": "Limpiar Búsqueda",
"memberPortalPublicResources": "Recursos Públicos",
"memberPortalPublicResourcesDescription": "Aplicaciones web y servicios accesibles vía navegador",
"memberPortalCopiedToClipboard": "Copiado al portapapeles",
"memberPortalCopiedUrlDescription": "La URL del recurso ha sido copiada a su portapapeles.",
"memberPortalOpenResource": "Abrir Recurso",
"memberPortalPrivateResources": "Recursos Privados",
"memberPortalPrivateResourcesDescription": "Recursos de red interna accesibles vía cliente",
"memberPortalResourceDetails": "Detalles del Recurso",
"memberPortalMode": "Modo",
"memberPortalDestination": "Destino",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "El alias del recurso ha sido copiado a su portapapeles.",
"memberPortalCopiedDestinationDescription": "El destino del recurso ha sido copiado a su portapapeles.",
"memberPortalRequiresClientConnection": "Requiere Conexión de Cliente",
"memberPortalAuthMethods": "Métodos de Autenticación",
"memberPortalSso": "Inicio de Sesión Único (SSO)",
"memberPortalPasswordProtected": "Protegido por Contraseña",
"memberPortalPinCode": "Código PIN",
"memberPortalEmailWhitelist": "Lista Blanca de Correo",
"memberPortalResourceDisabled": "Recurso Deshabilitado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
"memberPortalPrevious": "Anterior",
"memberPortalNext": "Siguiente"
"domainPendingErrorTitle": "Problema de verificación"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Vous dépassez vos limites pour votre forfait actuel. Corrigez le problème en supprimant des sites, des utilisateurs ou d'autres ressources pour rester dans votre forfait.",
"trialBannerMessage": "Votre essai expire dans {countdown}. Passez à l'abonnement pour garder l'accès.",
"trialBannerExpired": "Votre essai a expiré. Passez à l'abonnement maintenant pour restaurer l'accès.",
"billingTrialBannerTitle": "Essai gratuit actif",
"billingTrialBannerDescription": "Vous êtes actuellement en essai gratuit sur le niveau business. À la fin de l'essai, votre compte basculera automatiquement aux fonctionnalités et limites du niveau Basique. Mettez à jour à tout moment pour conserver l'accès aux fonctionnalités de votre plan actuel.",
"billingTrialBannerUpgrade": "Passer à la version supérieure maintenant",
"billingTrialBadge": "Essai gratuit",
"trialActive": "Essai gratuit actif",
"trialExpired": "Essai expiré",
"trialHasEnded": "Votre essai est terminé.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
"domainPickerWildcardCertWarningLink": "En savoir plus",
"health": "Santé",
"domainPendingErrorTitle": "Problème de vérification",
"memberPortalTitle": "Ressources",
"memberPortalDescription": "Ressources auxquelles vous avez accès dans cette organisation",
"memberPortalSortBy": "Trier par...",
"memberPortalSortNameAsc": "Nom A-Z",
"memberPortalSortNameDesc": "Nom Z-A",
"memberPortalSortDomainAsc": "Domaine A-Z",
"memberPortalSortDomainDesc": "Domaine Z-A",
"memberPortalSortEnabledFirst": "Activé en premier",
"memberPortalSortDisabledFirst": "Désactivé en premier",
"memberPortalRefresh": "Actualiser",
"memberPortalRefreshResources": "Actualiser les ressources",
"memberPortalFailedToLoad": "Échec du chargement des ressources",
"memberPortalFailedToLoadDescription": "Échec du chargement des ressources. Veuillez vérifier votre connexion et réessayer.",
"memberPortalUnableToLoad": "Impossible de charger les ressources",
"memberPortalTryAgain": "Réessayer",
"memberPortalNoResourcesFound": "Aucune ressource trouvée",
"memberPortalNoResourcesAvailable": "Aucune ressource disponible",
"memberPortalNoResourcesMatchSearch": "Aucune ressource ne correspond à \"{query}\". Essayez d'ajuster vos termes de recherche ou de vider la recherche pour voir toutes les ressources.",
"memberPortalNoResourcesAccess": "Vous n'avez encore accès à aucune ressource. Contactez votre administrateur pour obtenir l'accès aux ressources dont vous avez besoin.",
"memberPortalClearSearch": "Effacer la recherche",
"memberPortalPublicResources": "Ressources publiques",
"memberPortalPublicResourcesDescription": "Applications et services web accessibles via un navigateur",
"memberPortalCopiedToClipboard": "Copié dans le presse-papiers",
"memberPortalCopiedUrlDescription": "L'URL de la ressource a été copiée dans votre presse-papiers.",
"memberPortalOpenResource": "Ouvrir la ressource",
"memberPortalPrivateResources": "Ressources privées",
"memberPortalPrivateResourcesDescription": "Ressources réseau internes accessibles via un client",
"memberPortalResourceDetails": "Détails de la ressource",
"memberPortalMode": "Mode",
"memberPortalDestination": "Destination",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "L'alias de la ressource a été copié dans votre presse-papiers.",
"memberPortalCopiedDestinationDescription": "La destination de la ressource a été copiée dans votre presse-papiers.",
"memberPortalRequiresClientConnection": "Nécessite une connexion client",
"memberPortalAuthMethods": "Méthodes d'authentification",
"memberPortalSso": "Authentification unique (SSO)",
"memberPortalPasswordProtected": "Protégé par un mot de passe",
"memberPortalPinCode": "Code PIN",
"memberPortalEmailWhitelist": "Liste blanche des e-mails",
"memberPortalResourceDisabled": "Ressource désactivée",
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
"memberPortalPrevious": "Précédent",
"memberPortalNext": "Suivant"
"domainPendingErrorTitle": "Problème de vérification"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Hai superato i tuoi limiti per il tuo piano attuale. Correggi il problema rimuovendo siti, utenti o altre risorse per rimanere all'interno del tuo piano.",
"trialBannerMessage": "Il tuo periodo di prova scade tra {countdown}. Aggiorna per mantenere l'accesso.",
"trialBannerExpired": "Il tuo periodo di prova è scaduto. Aggiorna ora per ripristinare l'accesso.",
"billingTrialBannerTitle": "Prova Gratuita Attiva",
"billingTrialBannerDescription": "Attualmente sei in una prova gratuita sul livello business. Quando la prova terminerà, il tuo account tornerà automaticamente alle funzionalità e ai limiti del piano Basic. Effettua l'upgrade in qualsiasi momento per mantenere l'accesso alle funzionalità del tuo piano attuale.",
"billingTrialBannerUpgrade": "Effettua l'Upgrade Ora",
"billingTrialBadge": "Prova Gratuita",
"trialActive": "Prova Gratuita Attiva",
"trialExpired": "Prova scaduta",
"trialHasEnded": "La tua prova è terminata.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
"domainPickerWildcardCertWarningLink": "Scopri di più",
"health": "Salute",
"domainPendingErrorTitle": "Problema di Verifica",
"memberPortalTitle": "Risorse",
"memberPortalDescription": "Risorse a cui hai accesso in questa organizzazione",
"memberPortalSortBy": "Ordina per...",
"memberPortalSortNameAsc": "Nome A-Z",
"memberPortalSortNameDesc": "Nome Z-A",
"memberPortalSortDomainAsc": "Dominio A-Z",
"memberPortalSortDomainDesc": "Dominio Z-A",
"memberPortalSortEnabledFirst": "Abilitati per primi",
"memberPortalSortDisabledFirst": "Disabilitati per primi",
"memberPortalRefresh": "Aggiorna",
"memberPortalRefreshResources": "Aggiorna Risorse",
"memberPortalFailedToLoad": "Caricamento delle risorse non riuscito",
"memberPortalFailedToLoadDescription": "Caricamento delle risorse non riuscito. Controlla la tua connessione e riprova.",
"memberPortalUnableToLoad": "Impossibile caricare le risorse",
"memberPortalTryAgain": "Riprova",
"memberPortalNoResourcesFound": "Nessuna risorsa trovata",
"memberPortalNoResourcesAvailable": "Nessuna risorsa disponibile",
"memberPortalNoResourcesMatchSearch": "Nessuna risorsa corrisponde a \"{query}\". Prova ad aggiustare i termini di ricerca o a cancellare la ricerca per vedere tutte le risorse.",
"memberPortalNoResourcesAccess": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per ottenere l'accesso alle risorse di cui hai bisogno.",
"memberPortalClearSearch": "Cancella Ricerca",
"memberPortalPublicResources": "Risorse Pubbliche",
"memberPortalPublicResourcesDescription": "Applicazioni web e servizi accessibili tramite browser",
"memberPortalCopiedToClipboard": "Copiato negli appunti",
"memberPortalCopiedUrlDescription": "L'URL della risorsa è stato copiato negli appunti.",
"memberPortalOpenResource": "Apri Risorsa",
"memberPortalPrivateResources": "Risorse Private",
"memberPortalPrivateResourcesDescription": "Risorse di rete interne accessibili tramite client",
"memberPortalResourceDetails": "Dettagli della Risorsa",
"memberPortalMode": "Modalità",
"memberPortalDestination": "Destinazione",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "L'alias della risorsa è stato copiato negli appunti.",
"memberPortalCopiedDestinationDescription": "La destinazione della risorsa è stata copiata negli appunti.",
"memberPortalRequiresClientConnection": "Richiede Connessione Client",
"memberPortalAuthMethods": "Metodi di Autenticazione",
"memberPortalSso": "Accesso unico (Single Sign-On, SSO)",
"memberPortalPasswordProtected": "Protetto da password",
"memberPortalPinCode": "Codice PIN",
"memberPortalEmailWhitelist": "Lista Autorizzazioni Email",
"memberPortalResourceDisabled": "Risorsa Disabilitata",
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
"memberPortalPrevious": "Precedente",
"memberPortalNext": "Successivo"
"domainPendingErrorTitle": "Problema di Verifica"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "현재 계획의 한계를 초과했습니다. 사이트, 사용자 또는 기타 리소스를 제거하여 계획 내에 머물도록 해결하세요.",
"trialBannerMessage": "시험 사용 기간이 {countdown} 안에 만료됩니다. 업그레이드하여 액세스를 유지하세요.",
"trialBannerExpired": "시험 사용 기간이 만료되었습니다. 지금 업그레이드하여 액세스를 복구하세요.",
"billingTrialBannerTitle": "무료 평가판 활성화",
"billingTrialBannerDescription": "현재 비즈니스 티어의 무료 평가판을 사용 중입니다. 평가판이 종료되면 계정은 자동으로 기본 티어 기능 및 제한으로 돌아갑니다. 현재 계획의 기능을 유지하려면 언제든지 업그레이드 하세요.",
"billingTrialBannerUpgrade": "지금 업그레이드",
"billingTrialBadge": "무료 평가판",
"trialActive": "무료 체험 활성화됨",
"trialExpired": "체험 만료됨",
"trialHasEnded": "시험 사용 기간이 종료되었습니다.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
"health": "건강",
"domainPendingErrorTitle": "확인 문제",
"memberPortalTitle": "리소스",
"memberPortalDescription": "이 조직에서 접근할 수 있는 리소스",
"memberPortalSortBy": "정렬 기준...",
"memberPortalSortNameAsc": "이름 A-Z",
"memberPortalSortNameDesc": "이름 Z-A",
"memberPortalSortDomainAsc": "도메인 A-Z",
"memberPortalSortDomainDesc": "도메인 Z-A",
"memberPortalSortEnabledFirst": "사용 활성화 우선",
"memberPortalSortDisabledFirst": "사용 비활성화 우선",
"memberPortalRefresh": "새로 고침",
"memberPortalRefreshResources": "리소스 새로 고침",
"memberPortalFailedToLoad": "리소스를 불러오는 데 실패했습니다",
"memberPortalFailedToLoadDescription": "리소스를 불러오는 데 실패했습니다. 연결을 확인하고 다시 시도해 주십시오.",
"memberPortalUnableToLoad": "리소스를 가져오는 데 실패했습니다",
"memberPortalTryAgain": "다시 시도",
"memberPortalNoResourcesFound": "리소스를 발견하지 못했습니다",
"memberPortalNoResourcesAvailable": "사용 가능한 리소스가 없습니다",
"memberPortalNoResourcesMatchSearch": "\"{query}\"와 일치하는 리소스가 없습니다. 검색어를 수정하거나 검색을 초기화하여 모든 리소스를 확인하십시오.",
"memberPortalNoResourcesAccess": "아직 접근할 수 있는 리소스가 없습니다. 필요한 리소스 접근을 위해 관리자에게 문의하세요.",
"memberPortalClearSearch": "검색 초기화",
"memberPortalPublicResources": "공공 리소스",
"memberPortalPublicResourcesDescription": "브라우저를 통해 접근 가능한 웹 애플리케이션 및 서비스",
"memberPortalCopiedToClipboard": "클립보드에 복사됨",
"memberPortalCopiedUrlDescription": "리소스 URL이 클립보드에 복사되었습니다.",
"memberPortalOpenResource": "리소스 열기",
"memberPortalPrivateResources": "비공개 리소스",
"memberPortalPrivateResourcesDescription": "클라이언트를 통해 접근 가능한 내부 네트워크 리소스",
"memberPortalResourceDetails": "리소스 세부 정보",
"memberPortalMode": "모드",
"memberPortalDestination": "대상지",
"memberPortalAlias": "별칭",
"memberPortalCopiedAliasDescription": "리소스 별칭이 클립보드에 복사되었습니다.",
"memberPortalCopiedDestinationDescription": "리소스 대상지가 클립보드에 복사되었습니다.",
"memberPortalRequiresClientConnection": "클라이언트 연결 필요",
"memberPortalAuthMethods": "인증 방법",
"memberPortalSso": "싱글 사인온 (SSO)",
"memberPortalPasswordProtected": "비밀번호 보호",
"memberPortalPinCode": "PIN 코드",
"memberPortalEmailWhitelist": "이메일 화이트리스트",
"memberPortalResourceDisabled": "리소스 비활성화됨",
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
"memberPortalPrevious": "이전",
"memberPortalNext": "다음"
"domainPendingErrorTitle": "확인 문제"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Du er utenfor grensen for gjeldende plan. Rett problemet ved å fjerne nettsteder, brukere eller andre ressurser for å bli innenfor planen din.",
"trialBannerMessage": "Din prøveperiode utløper om {countdown}. Oppgrader for å beholde tilgangen.",
"trialBannerExpired": "Prøveperioden din har utløpt. Oppgrader nå for å gjenopprette tilgangen.",
"billingTrialBannerTitle": "Prøveversjon Aktiv",
"billingTrialBannerDescription": "Du har for øyeblikket en gratis prøveversjon på forretningsnivået. Når prøven avsluttes, vil kontoen din automatisk gå tilbake til funksjoner og begrensninger på Basis-nivået. Oppgrader når som helst for å beholde tilgang til de nåværende planens funksjoner.",
"billingTrialBannerUpgrade": "Oppgrader nå",
"billingTrialBadge": "Prøveversjon",
"trialActive": "Gratis prøveversjon aktiv",
"trialExpired": "Prøveperioden er utløpt",
"trialHasEnded": "Din prøveperiode har avsluttet.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
"domainPickerWildcardCertWarningLink": "Lær mer",
"health": "Helse",
"domainPendingErrorTitle": "Verifiseringsproblem",
"memberPortalTitle": "Ressurser",
"memberPortalDescription": "Ressurser du har tilgang til i denne organisasjonen",
"memberPortalSortBy": "Sorter etter...",
"memberPortalSortNameAsc": "Navn A-Å",
"memberPortalSortNameDesc": "Navn Å-A",
"memberPortalSortDomainAsc": "Domene A-Å",
"memberPortalSortDomainDesc": "Domene Å-A",
"memberPortalSortEnabledFirst": "Aktivert først",
"memberPortalSortDisabledFirst": "Deaktivert først",
"memberPortalRefresh": "Oppdater",
"memberPortalRefreshResources": "Oppdater ressurser",
"memberPortalFailedToLoad": "Kunne ikke laste inn ressurser",
"memberPortalFailedToLoadDescription": "Kunne ikke laste inn ressurser. Vennligst sjekk tilkoblingen din og prøv igjen.",
"memberPortalUnableToLoad": "Kan ikke laste inn ressurser",
"memberPortalTryAgain": "Prøv igjen",
"memberPortalNoResourcesFound": "Ingen ressurser funnet",
"memberPortalNoResourcesAvailable": "Ingen ressurser tilgjengelig",
"memberPortalNoResourcesMatchSearch": "Ingen ressurser samsvarer med \"{query}\". Prøv å justere søkeordene dine eller fjern søket for å se alle ressurser.",
"memberPortalNoResourcesAccess": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å få tilgang til de ressursene du trenger.",
"memberPortalClearSearch": "Fjern søk",
"memberPortalPublicResources": "Offentlige ressurser",
"memberPortalPublicResourcesDescription": "Webapplikasjoner og -tjenester tilgjengelige via nettleser",
"memberPortalCopiedToClipboard": "Kopiert til utklippstavlen",
"memberPortalCopiedUrlDescription": "Ressurs-URL er kopiert til utklippstavlen din.",
"memberPortalOpenResource": "Åpne ressurs",
"memberPortalPrivateResources": "Private ressurser",
"memberPortalPrivateResourcesDescription": "Interne nettverksressurser tilgjengelige via klient",
"memberPortalResourceDetails": "Ressursdetaljer",
"memberPortalMode": "Modus",
"memberPortalDestination": "Destinasjon",
"memberPortalAlias": "Navn",
"memberPortalCopiedAliasDescription": "Ressursalias er kopiert til utklippstavlen din.",
"memberPortalCopiedDestinationDescription": "Ressursdestinasjon er kopiert til utklippstavlen din.",
"memberPortalRequiresClientConnection": "Krever klienttilkobling",
"memberPortalAuthMethods": "Autentiseringsmetoder",
"memberPortalSso": "Enkeltpålogging (SSO)",
"memberPortalPasswordProtected": "Passordbeskyttet",
"memberPortalPinCode": "PIN-kode",
"memberPortalEmailWhitelist": "E-post-hviteliste",
"memberPortalResourceDisabled": "Ressurs deaktivert",
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
"memberPortalPrevious": "Forrige",
"memberPortalNext": "Neste"
"domainPendingErrorTitle": "Verifiseringsproblem"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "U overschrijdt uw huidige abonnement. Corrigeer het probleem door sites, gebruikers of andere bronnen te verwijderen om binnen uw plan te blijven.",
"trialBannerMessage": "Uw proefversie verloopt over {countdown}. Upgrade om toegang te behouden.",
"trialBannerExpired": "Uw proefperiode is verlopen. Upgrade nu om toegang te herstellen.",
"billingTrialBannerTitle": "Proefperiode Actief",
"billingTrialBannerDescription": "Je bent momenteel bezig met een gratis proefperiode op het zakelijke niveau. Wanneer de proefperiode eindigt, wordt je account automatisch teruggezet naar de functies en limieten van het Basic-niveau. Upgrade op elk moment om toegang te houden tot de functies van je huidige plan.",
"billingTrialBannerUpgrade": "Nu Upgraden",
"billingTrialBadge": "Gratis Proefversie",
"trialActive": "Gratis proefversie actief",
"trialExpired": "Proefversie verlopen",
"trialHasEnded": "Uw proefperiode is geëindigd.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
"domainPickerWildcardCertWarningLink": "Meer informatie",
"health": "Gezondheid",
"domainPendingErrorTitle": "Verificatieprobleem",
"memberPortalTitle": "Bronnen",
"memberPortalDescription": "Bronnen waartoe je toegang hebt binnen deze organisatie",
"memberPortalSortBy": "Sorteren op...",
"memberPortalSortNameAsc": "Naam A-Z",
"memberPortalSortNameDesc": "Naam Z-A",
"memberPortalSortDomainAsc": "Domein A-Z",
"memberPortalSortDomainDesc": "Domein Z-A",
"memberPortalSortEnabledFirst": "Ingeschakeld Eerst",
"memberPortalSortDisabledFirst": "Uitgeschakeld Eerst",
"memberPortalRefresh": "Vernieuwen",
"memberPortalRefreshResources": "Bronnen Vernieuwen",
"memberPortalFailedToLoad": "Fout bij het laden van bronnen",
"memberPortalFailedToLoadDescription": "Fout bij het laden van bronnen. Controleer uw verbinding en probeer het opnieuw.",
"memberPortalUnableToLoad": "Niet in staat om bronnen te laden",
"memberPortalTryAgain": "Probeer Opnieuw",
"memberPortalNoResourcesFound": "Geen Bronnen Gevonden",
"memberPortalNoResourcesAvailable": "Geen Bronnen Beschikbaar",
"memberPortalNoResourcesMatchSearch": "Geen bronnen komen overeen met \"{query}\". Probeer uw zoektermen aan te passen of wis de zoekopdracht om alle bronnen te zien.",
"memberPortalNoResourcesAccess": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang te krijgen tot de benodigde bronnen.",
"memberPortalClearSearch": "Zoekopdracht Wissen",
"memberPortalPublicResources": "Publieke Bronnen",
"memberPortalPublicResourcesDescription": "Webapplicaties en services toegankelijk via browser",
"memberPortalCopiedToClipboard": "Gekopieerd naar klembord",
"memberPortalCopiedUrlDescription": "Bron URL is naar uw klembord gekopieerd.",
"memberPortalOpenResource": "Bron Openen",
"memberPortalPrivateResources": "Privé Bronnen",
"memberPortalPrivateResourcesDescription": "Interne netwerkbronnen toegankelijk via client",
"memberPortalResourceDetails": "Bron Details",
"memberPortalMode": "Modus",
"memberPortalDestination": "Bestemming",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "Bron alias is naar uw klembord gekopieerd.",
"memberPortalCopiedDestinationDescription": "Bron bestemming is naar uw klembord gekopieerd.",
"memberPortalRequiresClientConnection": "Clientverbinding Vereist",
"memberPortalAuthMethods": "Authenticatiemethoden",
"memberPortalSso": "Single Sign-On (SSO)",
"memberPortalPasswordProtected": "Wachtwoord Beveiligd",
"memberPortalPinCode": "Pincode",
"memberPortalEmailWhitelist": "E-mail whitelist",
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
"memberPortalPrevious": "Vorige",
"memberPortalNext": "Volgende"
"domainPendingErrorTitle": "Verificatieprobleem"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Nie masz ograniczeń dla aktualnego planu. Popraw problem poprzez usunięcie stron, użytkowników lub innych zasobów, aby pozostać w swoim planie.",
"trialBannerMessage": "Twój okres próbny wygasa za {countdown}. Uaktualnij, aby zachować dostęp.",
"trialBannerExpired": "Twój okres próbny wygasł. Uaktualnij teraz, aby przywrócić dostęp.",
"billingTrialBannerTitle": "Bezpłatna wersja próbna aktywna",
"billingTrialBannerDescription": "Obecnie korzystasz z bezpłatnej wersji próbnej na poziomie biznesowym. Po zakończeniu wersji próbnej, Twoje konto automatycznie powróci do funkcji i limitów poziomu Podstawowego. Możesz dokonać uaktualnienia w każdej chwili, aby zachować dostęp do funkcji obecnego planu.",
"billingTrialBannerUpgrade": "Uaktualnij teraz",
"billingTrialBadge": "Bezpłatna wersja próbna",
"trialActive": "Okres próbny aktywny",
"trialExpired": "Okres próbny wygasł",
"trialHasEnded": "Twój okres próbny dobiegł końca.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
"health": "Zdrowie",
"domainPendingErrorTitle": "Problem z weryfikacją",
"memberPortalTitle": "Zasoby",
"memberPortalDescription": "Zasoby, do których masz dostęp w tej organizacji",
"memberPortalSortBy": "Sortuj według...",
"memberPortalSortNameAsc": "Nazwa A-Z",
"memberPortalSortNameDesc": "Nazwa Z-A",
"memberPortalSortDomainAsc": "Domena A-Z",
"memberPortalSortDomainDesc": "Domena Z-A",
"memberPortalSortEnabledFirst": "Włączone najpierw",
"memberPortalSortDisabledFirst": "Wyłączone najpierw",
"memberPortalRefresh": "Odśwież",
"memberPortalRefreshResources": "Odśwież zasoby",
"memberPortalFailedToLoad": "Nie udało się załadować zasobów",
"memberPortalFailedToLoadDescription": "Nie udało się załadować zasobów. Sprawdź połączenie i spróbuj ponownie.",
"memberPortalUnableToLoad": "Nie można załadować zasobów",
"memberPortalTryAgain": "Spróbuj ponownie",
"memberPortalNoResourcesFound": "Nie znaleziono zasobów",
"memberPortalNoResourcesAvailable": "Brak dostępnych zasobów",
"memberPortalNoResourcesMatchSearch": "Żadne zasoby nie pasują do „{query}”. Spróbuj dostosować swoje warunki wyszukiwania lub wyczyść wyszukiwanie, aby zobaczyć wszystkie zasoby.",
"memberPortalNoResourcesAccess": "Nie masz jeszcze dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby uzyskać dostęp do potrzebnych zasobów.",
"memberPortalClearSearch": "Wyczyść wyszukiwanie",
"memberPortalPublicResources": "Publiczne zasoby",
"memberPortalPublicResourcesDescription": "Aplikacje i usługi internetowe dostępne za pośrednictwem przeglądarki",
"memberPortalCopiedToClipboard": "Skopiowano do schowka",
"memberPortalCopiedUrlDescription": "URL zasobu został skopiowany do schowka.",
"memberPortalOpenResource": "Otwórz zasób",
"memberPortalPrivateResources": "Prywatne zasoby",
"memberPortalPrivateResourcesDescription": "Zasoby sieci wewnętrznej dostępne za pośrednictwem klienta",
"memberPortalResourceDetails": "Szczegóły zasobu",
"memberPortalMode": "Tryb",
"memberPortalDestination": "Miejsce docelowe",
"memberPortalAlias": "Pseudonim",
"memberPortalCopiedAliasDescription": "Alias zasobu został skopiowany do schowka.",
"memberPortalCopiedDestinationDescription": "Miejsce docelowe zasobu zostało skopiowane do schowka.",
"memberPortalRequiresClientConnection": "Wymaga połączenia z klientem",
"memberPortalAuthMethods": "Metody uwierzytelniania",
"memberPortalSso": "Jednorazowe logowanie (SSO)",
"memberPortalPasswordProtected": "Chronione hasłem",
"memberPortalPinCode": "Kod PIN",
"memberPortalEmailWhitelist": "Biała lista e-mail",
"memberPortalResourceDisabled": "Zasób wyłączony",
"memberPortalShowingResources": "Wyświetlanie zasobów od {start} do {end} z {total}",
"memberPortalPrevious": "Poprzedni",
"memberPortalNext": "Następny"
"domainPendingErrorTitle": "Problem z weryfikacją"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Você está além dos seus limites para o seu plano atual. Corrija o problema removendo sites, usuários, ou outros recursos para ficar em seu plano.",
"trialBannerMessage": "Sua avaliação termina em {countdown}. Faça o upgrade para manter o acesso.",
"trialBannerExpired": "Sua avaliação expirou. Faça o upgrade agora para restaurar o acesso.",
"billingTrialBannerTitle": "Teste Gratuito Ativo",
"billingTrialBannerDescription": "Atualmente, você está em um teste gratuito no nível empresarial. Quando o teste terminar, sua conta reverterá automaticamente para os recursos e limites do nível Básico. Atualize a qualquer momento para manter o acesso aos recursos do seu plano atual.",
"billingTrialBannerUpgrade": "Atualize Agora",
"billingTrialBadge": "Teste Gratuito",
"trialActive": "Avaliação Gratuita Ativa",
"trialExpired": "Avaliação Expirada",
"trialHasEnded": "Sua avaliação terminou.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
"domainPickerWildcardCertWarningLink": "Saiba mais",
"health": "Saúde",
"domainPendingErrorTitle": "Problema de Verificação",
"memberPortalTitle": "Recursos",
"memberPortalDescription": "Recursos aos quais você tem acesso nesta organização",
"memberPortalSortBy": "Ordenar por...",
"memberPortalSortNameAsc": "Nome A-Z",
"memberPortalSortNameDesc": "Nome Z-A",
"memberPortalSortDomainAsc": "Domínio A-Z",
"memberPortalSortDomainDesc": "Domínio Z-A",
"memberPortalSortEnabledFirst": "Habilitados Primeiro",
"memberPortalSortDisabledFirst": "Desabilitados Primeiro",
"memberPortalRefresh": "Atualizar",
"memberPortalRefreshResources": "Atualizar Recursos",
"memberPortalFailedToLoad": "Falha ao carregar recursos",
"memberPortalFailedToLoadDescription": "Falha ao carregar recursos. Por favor, verifique sua conexão e tente novamente.",
"memberPortalUnableToLoad": "Incapaz de Carregar Recursos",
"memberPortalTryAgain": "Tentar Novamente",
"memberPortalNoResourcesFound": "Nenhum Recurso Encontrado",
"memberPortalNoResourcesAvailable": "Nenhum Recurso Disponível",
"memberPortalNoResourcesMatchSearch": "Nenhum recurso corresponde a \"{query}\". Tente ajustar seus termos de pesquisa ou limpe a pesquisa para ver todos os recursos.",
"memberPortalNoResourcesAccess": "Você ainda não tem acesso a nenhum recurso. Entre em contato com seu administrador para obter acesso aos recursos que precisa.",
"memberPortalClearSearch": "Limpar Pesquisa",
"memberPortalPublicResources": "Recursos Públicos",
"memberPortalPublicResourcesDescription": "Aplicações e serviços web acessíveis via navegador",
"memberPortalCopiedToClipboard": "Copiado para a área de transferência",
"memberPortalCopiedUrlDescription": "A URL do recurso foi copiada para sua área de transferência.",
"memberPortalOpenResource": "Abrir Recurso",
"memberPortalPrivateResources": "Recursos Privados",
"memberPortalPrivateResourcesDescription": "Recursos da rede interna acessíveis via cliente",
"memberPortalResourceDetails": "Detalhes do Recurso",
"memberPortalMode": "Modo",
"memberPortalDestination": "Destino",
"memberPortalAlias": "Apelido",
"memberPortalCopiedAliasDescription": "O apelido do recurso foi copiado para sua área de transferência.",
"memberPortalCopiedDestinationDescription": "O destino do recurso foi copiado para sua área de transferência.",
"memberPortalRequiresClientConnection": "Requer Conexão de Cliente",
"memberPortalAuthMethods": "Métodos de Autenticação",
"memberPortalSso": "Logon Único (SSO)",
"memberPortalPasswordProtected": "Protegido por Senha",
"memberPortalPinCode": "Código PIN",
"memberPortalEmailWhitelist": "Lista de E-mails Permitidos",
"memberPortalResourceDisabled": "Recurso Desativado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
"memberPortalPrevious": "Anterior",
"memberPortalNext": "Próximo"
"domainPendingErrorTitle": "Problema de Verificação"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Вы превысили лимиты для вашего текущего плана. Исправьте проблему, удалив сайты, пользователей или другие ресурсы, чтобы остаться в пределах вашего плана.",
"trialBannerMessage": "Ваш пробный период истекает через {countdown}. Обновите, чтобы сохранить доступ.",
"trialBannerExpired": "Ваш пробный период истек. Обновите сейчас, чтобы восстановить доступ.",
"billingTrialBannerTitle": "Бесплатная версия активна",
"billingTrialBannerDescription": "Вы в настоящее время находитесь на бесплатном пробном периоде бизнес-уровня. Когда пробный период закончится, ваш аккаунт автоматически вернётся к функциям и лимитам базового уровня. Обновите в любое время, чтобы сохранить доступ к функциям текущего плана.",
"billingTrialBannerUpgrade": "Обновить сейчас",
"billingTrialBadge": "Бесплатная версия",
"trialActive": "Бесплатный пробный период активен",
"trialExpired": "Пробный период истек",
"trialHasEnded": "Ваш пробный период окончен.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
"domainPickerWildcardCertWarningLink": "Узнать больше",
"health": "Состояние",
"domainPendingErrorTitle": "Проблема с подтверждением",
"memberPortalTitle": "Ресурсы",
"memberPortalDescription": "Ресурсы, к которым у вас есть доступ в этой организации",
"memberPortalSortBy": "Сортировать по...",
"memberPortalSortNameAsc": "Имя A-Я",
"memberPortalSortNameDesc": "Имя Я-A",
"memberPortalSortDomainAsc": "Домен A-Я",
"memberPortalSortDomainDesc": "Домен Я-A",
"memberPortalSortEnabledFirst": "Включённые сначала",
"memberPortalSortDisabledFirst": "Отключённые сначала",
"memberPortalRefresh": "Обновить",
"memberPortalRefreshResources": "Обновить ресурсы",
"memberPortalFailedToLoad": "Не удалось загрузить ресурсы",
"memberPortalFailedToLoadDescription": "Не удалось загрузить ресурсы. Пожалуйста, проверьте подключение и попробуйте снова.",
"memberPortalUnableToLoad": "Не удалось загрузить ресурсы",
"memberPortalTryAgain": "Попробуйте снова",
"memberPortalNoResourcesFound": "Ресурсы не найдены",
"memberPortalNoResourcesAvailable": "Нет доступных ресурсов",
"memberPortalNoResourcesMatchSearch": "Нет ресурсов, соответствующих \"{query}\". Попробуйте изменить условия поиска или очистить поиск, чтобы увидеть все ресурсы.",
"memberPortalNoResourcesAccess": "У вас пока нет доступа к ресурсам. Свяжитесь с администратором, чтобы получить доступ к нужным вам ресурсам.",
"memberPortalClearSearch": "Очистить поиск",
"memberPortalPublicResources": "Публичные ресурсы",
"memberPortalPublicResourcesDescription": "Веб-приложения и сервисы, доступные через браузер",
"memberPortalCopiedToClipboard": "Скопировано в буфер обмена",
"memberPortalCopiedUrlDescription": "URL ресурса был скопирован в ваш буфер обмена.",
"memberPortalOpenResource": "Открыть ресурс",
"memberPortalPrivateResources": "Приватные ресурсы",
"memberPortalPrivateResourcesDescription": "Ресурсы внутренней сети, доступные через клиент",
"memberPortalResourceDetails": "Детали ресурса",
"memberPortalMode": "Режим",
"memberPortalDestination": "Назначение",
"memberPortalAlias": "Псевдоним",
"memberPortalCopiedAliasDescription": "Псевдоним ресурса был скопирован в ваш буфер обмена.",
"memberPortalCopiedDestinationDescription": "Назначение ресурса было скопировано в ваш буфер обмена.",
"memberPortalRequiresClientConnection": "Требуется подключение клиента",
"memberPortalAuthMethods": "Методы аутентификации",
"memberPortalSso": "Единый вход (SSO)",
"memberPortalPasswordProtected": "Защищено паролем",
"memberPortalPinCode": "PIN-код",
"memberPortalEmailWhitelist": "Белый список email",
"memberPortalResourceDisabled": "Ресурс отключён",
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
"memberPortalPrevious": "Предыдущий",
"memberPortalNext": "Следующий"
"domainPendingErrorTitle": "Проблема с подтверждением"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "Geçerli planınız için limitlerinizi aştınız. Planınız dahilinde kalmak için siteleri, kullanıcıları veya diğer kaynakları kaldırarak sorunu düzeltin.",
"trialBannerMessage": "Deneme süreniz {countdown} içinde sona eriyor. Erişimi sürdürmek için yükseltin.",
"trialBannerExpired": "Deneme süreniz sona erdi. Erişimi geri yüklemek için şimdi yükseltin.",
"billingTrialBannerTitle": "Ücretsiz Deneme Aktif",
"billingTrialBannerDescription": "Şu anda iş seviyesi için ücretsiz deneme sürümündesiniz. Deneme süresi sona erdiğinde, hesabınız otomatik olarak Temel seviye özelliklerine ve limitlerine geri dönecektir. Mevcut planınızın özelliklerine erişimi sürdürmek için istediğiniz zaman yükseltin.",
"billingTrialBannerUpgrade": "Şimdi Yükselt",
"billingTrialBadge": "Ücretsiz Deneme",
"trialActive": "Ücretsiz Deneme Aktif",
"trialExpired": "Deneme Süresi Doldu",
"trialHasEnded": "Deneme süreniz sona erdi.",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
"health": "Sağlık",
"domainPendingErrorTitle": "Doğrulama Sorunu",
"memberPortalTitle": "Kaynaklar",
"memberPortalDescription": "Bu organizasyondaki erişiminiz olan kaynaklar",
"memberPortalSortBy": "Şuna göre sırala...",
"memberPortalSortNameAsc": "İsim A-Z",
"memberPortalSortNameDesc": "İsim Z-A",
"memberPortalSortDomainAsc": "Alan A-Z",
"memberPortalSortDomainDesc": "Alan Z-A",
"memberPortalSortEnabledFirst": "İlk Etkinleştirilenler",
"memberPortalSortDisabledFirst": "İlk Devre Dışı Bırakılanlar",
"memberPortalRefresh": "Yenile",
"memberPortalRefreshResources": "Kaynakları Yenile",
"memberPortalFailedToLoad": "Kaynaklar yüklenemedi",
"memberPortalFailedToLoadDescription": "Kaynaklar yüklenemedi. Lütfen bağlantınızı kontrol edin ve tekrar deneyin.",
"memberPortalUnableToLoad": "Kaynaklar Yüklenemiyor",
"memberPortalTryAgain": "Tekrar Dene",
"memberPortalNoResourcesFound": "Hiçbir Kaynak Bulunamadı",
"memberPortalNoResourcesAvailable": "Uygun Kaynak Yok",
"memberPortalNoResourcesMatchSearch": "Hiçbir kaynak \"{query}\" ile eşleşmiyor. Arama terimlerinizi değiştirerek veya tüm kaynakları görmek için aramayı temizleyerek deneyin.",
"memberPortalNoResourcesAccess": "Henüz herhangi bir kaynağa erişiminiz yok. İhtiyacınız olan kaynaklara erişim sağlamak için yöneticinizle iletişime geçin.",
"memberPortalClearSearch": "Aramayı Temizle",
"memberPortalPublicResources": "Genel Kaynaklar",
"memberPortalPublicResourcesDescription": "Tarayıcı üzerinden erişilebilen web uygulamaları ve hizmetler",
"memberPortalCopiedToClipboard": "Panoya kopyalandı",
"memberPortalCopiedUrlDescription": "Kaynak URL'si panonuza kopyalandı.",
"memberPortalOpenResource": "Kaynağı Aç",
"memberPortalPrivateResources": "Özel Kaynaklar",
"memberPortalPrivateResourcesDescription": "İstemci üzerinden erişilebilen dahili ağ kaynakları",
"memberPortalResourceDetails": "Kaynak Detayları",
"memberPortalMode": "Mod",
"memberPortalDestination": "Hedef",
"memberPortalAlias": "Takma İsim",
"memberPortalCopiedAliasDescription": "Kaynak takma adı panonuza kopyalandı.",
"memberPortalCopiedDestinationDescription": "Kaynak hedefi panonuza kopyalandı.",
"memberPortalRequiresClientConnection": "İstemci Bağlantısı Gerektirir",
"memberPortalAuthMethods": "Kimlik Doğrulama Yöntemleri",
"memberPortalSso": "Tek Oturum Açma (SSO)",
"memberPortalPasswordProtected": "Parola ile Korunan",
"memberPortalPinCode": "PIN Kodu",
"memberPortalEmailWhitelist": "E-posta Beyaz Listesi",
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
"memberPortalPrevious": "Önceki",
"memberPortalNext": "Sonraki"
"domainPendingErrorTitle": "Doğrulama Sorunu"
}
+1 -48
View File
@@ -25,10 +25,6 @@
"subscriptionViolationMessage": "您的当前计划超出了您的限制。通过移除站点、用户或其他资源以保持在您的计划范围内来纠正问题。",
"trialBannerMessage": "您的试用将在 {countdown} 到期。升级以保持访问。",
"trialBannerExpired": "您的试用已到期。立即升级以恢复访问。",
"billingTrialBannerTitle": "免费试用激活中",
"billingTrialBannerDescription": "您目前正在商用层进行免费试用。试用结束后,您的账户将自动回到基础层功能和限制。可随时升级以保持当前计划的功能访问。",
"billingTrialBannerUpgrade": "立即升级",
"billingTrialBadge": "免费试用",
"trialActive": "免费试用中",
"trialExpired": "试用到期",
"trialHasEnded": "您的试用已结束。",
@@ -3208,48 +3204,5 @@
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
"domainPickerWildcardCertWarningLink": "了解更多",
"health": "健康",
"domainPendingErrorTitle": "验证问题",
"memberPortalTitle": "资源",
"memberPortalDescription": "您在此组织中可以访问的资源",
"memberPortalSortBy": "排序依据……",
"memberPortalSortNameAsc": "名称 A-Z",
"memberPortalSortNameDesc": "名称 Z-A",
"memberPortalSortDomainAsc": "域名 A-Z",
"memberPortalSortDomainDesc": "域名 Z-A",
"memberPortalSortEnabledFirst": "启用优先",
"memberPortalSortDisabledFirst": "禁用优先",
"memberPortalRefresh": "刷新",
"memberPortalRefreshResources": "刷新资源",
"memberPortalFailedToLoad": "加载资源失败",
"memberPortalFailedToLoadDescription": "加载资源失败。请检查您的连接并再试一次。",
"memberPortalUnableToLoad": "无法加载资源",
"memberPortalTryAgain": "再试一次",
"memberPortalNoResourcesFound": "找不到资源",
"memberPortalNoResourcesAvailable": "无可用资源",
"memberPortalNoResourcesMatchSearch": "没有与\"{query}\"匹配的资源。尝试调整您的搜索词或清除搜索以查看所有资源。",
"memberPortalNoResourcesAccess": "您尚无访问任何资源的权限。请联系您的管理员获取所需资源的访问权限。",
"memberPortalClearSearch": "清除搜索",
"memberPortalPublicResources": "公共资源",
"memberPortalPublicResourcesDescription": "通过浏览器可访问的网络应用和服务",
"memberPortalCopiedToClipboard": "已复制到剪贴板",
"memberPortalCopiedUrlDescription": "资源 URL 已复制到您的剪贴板。",
"memberPortalOpenResource": "打开资源",
"memberPortalPrivateResources": "私有资源",
"memberPortalPrivateResourcesDescription": "通过客户端可访问的内部网络资源",
"memberPortalResourceDetails": "资源详情",
"memberPortalMode": "模式",
"memberPortalDestination": "目标",
"memberPortalAlias": "别名",
"memberPortalCopiedAliasDescription": "资源别名已复制到您的剪贴板。",
"memberPortalCopiedDestinationDescription": "资源目的地已复制到您的剪贴板。",
"memberPortalRequiresClientConnection": "需要客户端连接",
"memberPortalAuthMethods": "身份验证方法",
"memberPortalSso": "单一登录 (SSO)",
"memberPortalPasswordProtected": "密码保护",
"memberPortalPinCode": "PIN 码",
"memberPortalEmailWhitelist": "电子邮件白名单",
"memberPortalResourceDisabled": "资源已禁用",
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
"memberPortalPrevious": "上一页",
"memberPortalNext": "下一页"
"domainPendingErrorTitle": "验证问题"
}
@@ -16,7 +16,6 @@ import { customers, db, subscriptions } from "@server/db";
import { eq } from "drizzle-orm";
import logger from "@server/logger";
import { generateId } from "@server/auth/sessions/app";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
export async function handleCustomerCreated(
customer: Stripe.Customer
@@ -63,13 +62,6 @@ export async function handleCustomerCreated(
expiresAt: trialExpiresAt,
trial: true
});
// update to the business limits for the trial
await handleSubscriptionLifesycle(
customer.metadata.orgId,
"active",
"tier3"
);
});
logger.info(`Customer with ID ${customer.id} created successfully.`);
@@ -44,7 +44,7 @@ function getLimitSetForSubscriptionType(
export async function handleSubscriptionLifesycle(
orgId: string,
status: string,
subType: SubscriptionType | null = null
subType: SubscriptionType | null
) {
switch (status) {
case "active":
@@ -90,13 +90,14 @@ export async function createCertificate(
domainToWrite = `*.${domainToWrite}`;
}
} else if (domainRecord.type == "ns") {
if (domain == domainRecord.baseDomain) {
domainToWrite = domainRecord.baseDomain;
} else {
const parts = domain.split(".");
if (parts.length > 2) {
domainToWrite = parts.slice(1).join(".");
}
// first if we have a * in the domain for this case we dont want to include it because it will mess with the cert generator so remove it
if (domain.startsWith("*.")) {
domain = domain.slice(2);
}
const parts = domain.split(".");
if (parts.length > 2) {
domainToWrite = parts.slice(1).join(".");
}
}
@@ -24,18 +24,13 @@ import { fromError } from "zod-validation-error";
import { sendEmail } from "@server/emails";
import NotifyTrialExpiring from "@server/emails/templates/NotifyTrialExpiring";
import config from "@server/lib/config";
import { handleSubscriptionLifesycle } from "../billing/subscriptionLifecycle";
const sendTrialNotificationParamsSchema = z.object({
orgId: z.string()
});
const sendTrialNotificationBodySchema = z.object({
notificationType: z.enum([
"trial_ending_5d",
"trial_ending_24h",
"trial_ended"
]),
notificationType: z.enum(["trial_ending_5d", "trial_ending_24h", "trial_ended"]),
orgName: z.string(),
trialEndsAt: z.number(),
billingLink: z.string().optional()
@@ -74,7 +69,9 @@ async function getOrgAdmins(orgId: string) {
)
);
const byUserId = new Map(admins.map((a) => [a.userId, a]));
const byUserId = new Map(
admins.map((a) => [a.userId, a])
);
const orgAdmins = Array.from(byUserId.values()).filter(
(admin) => admin.email && admin.email.length > 0
);
@@ -111,12 +108,8 @@ export async function sendTrialNotification(
}
const { orgId } = parsedParams.data;
const {
notificationType,
orgName,
trialEndsAt,
billingLink: bodyBillingLink
} = parsedBody.data;
const { notificationType, orgName, trialEndsAt, billingLink: bodyBillingLink } =
parsedBody.data;
// Verify organization exists
const org = await db
@@ -153,17 +146,13 @@ export async function sendTrialNotification(
bodyBillingLink ??
`${config.getRawConfig().app.dashboard_url}/${orgId}/settings/billing`;
const trialEndsAtFormatted = new Date(
trialEndsAt * 1000
).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
const trialEndsAtFormatted = new Date(trialEndsAt * 1000).toLocaleDateString(
"en-US",
{ year: "numeric", month: "long", day: "numeric" }
);
let daysRemaining: number | null;
let subject: string;
let resetLimits = false;
if (notificationType === "trial_ending_5d") {
daysRemaining = 5;
@@ -174,7 +163,6 @@ export async function sendTrialNotification(
} else {
daysRemaining = null;
subject = "Your trial has ended";
resetLimits = true;
}
let emailsSent = 0;
@@ -213,14 +201,6 @@ export async function sendTrialNotification(
}
}
if (resetLimits) {
// this will only fire if they have not upgraded yet because when upgrading we delete the trial
await handleSubscriptionLifesycle(orgId, "cancled");
logger.debug(
`Trial ended for org ${orgId}, limits reset to free tier`
);
}
return response<SendTrialNotificationResponse>(res, {
data: {
success: true,
@@ -241,4 +221,4 @@ export async function sendTrialNotification(
)
);
}
}
}
+7 -50
View File
@@ -1003,11 +1003,7 @@ async function checkRules(
isIpInCidr(clientIp, rule.value)
) {
return rule.action as any;
} else if (
clientIp &&
rule.match == "IP" &&
clientIp == rule.value
) {
} else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
return rule.action as any;
} else if (
path &&
@@ -1017,35 +1013,16 @@ async function checkRules(
return rule.action as any;
} else if (
clientIp &&
rule.match == "COUNTRY"
rule.match == "COUNTRY" &&
(await isIpInGeoIP(ipCC, rule.value))
) {
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
if (
rule.value.toUpperCase() === "ALL" &&
isLocalOrCarrierGradeNatIp(clientIp)
) {
continue;
}
if (await isIpInGeoIP(ipCC, rule.value)) {
return rule.action as any;
}
return rule.action as any;
} else if (
clientIp &&
rule.match == "ASN"
rule.match == "ASN" &&
(await isIpInAsn(ipAsn, rule.value))
) {
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
if (
(rule.value.toUpperCase() === "ALL" ||
rule.value.toUpperCase() === "AS0") &&
isLocalOrCarrierGradeNatIp(clientIp)
) {
continue;
}
if (await isIpInAsn(ipAsn, rule.value)) {
return rule.action as any;
}
return rule.action as any;
} else if (
clientIp &&
rule.match == "REGION" &&
@@ -1207,26 +1184,6 @@ async function isIpInGeoIP(
return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase();
}
function isLocalOrCarrierGradeNatIp(ip: string): boolean {
const localAndCgnatCidrs = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"::1/128",
"fc00::/7",
"fe80::/10"
];
try {
return localAndCgnatCidrs.some((cidr) => isIpInCidr(ip, cidr));
} catch {
return false;
}
}
async function isIpInAsn(
ipAsn: number | undefined,
checkAsn: string
+32 -27
View File
@@ -38,7 +38,10 @@ import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsFor
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { assignUserToOrg, removeUserFromOrg } from "@server/lib/userOrg";
import {
assignUserToOrg,
removeUserFromOrg
} from "@server/lib/userOrg";
import { unwrapRoleMapping } from "@app/lib/idpRoleMapping";
const ensureTrailingSlash = (url: string): string => {
@@ -333,23 +336,23 @@ export async function validateOidcCallback(
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
allOrgs = idpOrgs.map((o) => o.orgs);
// for (const org of allOrgs) {
// const subscribed = await isSubscribed(
// org.orgId,
// tierMatrix.autoProvisioning
// );
// if (!subscribed) {
// // filter out the org
// allOrgs = allOrgs.filter((o) => o.orgId !== org.orgId);
for (const org of allOrgs) {
const subscribed = await isSubscribed(
org.orgId,
tierMatrix.autoProvisioning
);
if (!subscribed) {
// filter out the org
allOrgs = allOrgs.filter((o) => o.orgId !== org.orgId);
// // return next(
// // createHttpError(
// // HttpCode.FORBIDDEN,
// // "This organization's current plan does not support this feature."
// // )
// // );
// }
// }
// return next(
// createHttpError(
// HttpCode.FORBIDDEN,
// "This organization's current plan does not support this feature."
// )
// );
}
}
} else {
allOrgs = await db.select().from(orgs);
}
@@ -393,14 +396,16 @@ export async function validateOidcCallback(
idpOrgRes?.roleMapping || defaultRoleMapping;
if (roleMapping) {
logger.debug("Role Mapping", { roleMapping });
const roleMappingJmes =
unwrapRoleMapping(roleMapping).evaluationExpression;
const roleMappingJmes = unwrapRoleMapping(
roleMapping
).evaluationExpression;
const roleMappingResult = jmespath.search(
claims,
roleMappingJmes
);
const roleNames =
normalizeRoleMappingResult(roleMappingResult);
const roleNames = normalizeRoleMappingResult(
roleMappingResult
);
const supportsMultiRole = await isLicensedOrSubscribed(
org.orgId,
@@ -510,7 +515,7 @@ export async function validateOidcCallback(
}
}
const orgUserCounts: { orgId: string; userCount: number }[] = [];
const orgUserCounts: { orgId: string; userCount: number }[] = [];
// sync the user with the orgs and roles
await db.transaction(async (trx) => {
@@ -623,7 +628,7 @@ export async function validateOidcCallback(
{
orgId: org.orgId,
userId: userId!,
autoProvisioned: true
autoProvisioned: true,
},
org.roleIds,
trx
@@ -753,7 +758,9 @@ function hydrateOrgMapping(
return orgMapping.split("{{orgId}}").join(orgId);
}
function normalizeRoleMappingResult(result: unknown): string[] {
function normalizeRoleMappingResult(
result: unknown
): string[] {
if (typeof result === "string") {
const role = result.trim();
return role ? [role] : [];
@@ -763,9 +770,7 @@ function normalizeRoleMappingResult(result: unknown): string[] {
return [
...new Set(
result
.filter(
(value): value is string => typeof value === "string"
)
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean)
)
-2
View File
@@ -56,8 +56,6 @@ function queryTargets(resourceId: number) {
hcStatus: targetHealthCheck.hcStatus,
hcHealth: targetHealthCheck.hcHealth,
hcTlsServerName: targetHealthCheck.hcTlsServerName,
hcHealthyThreshold: targetHealthCheck.hcHealthyThreshold,
hcUnhealthyThreshold: targetHealthCheck.hcUnhealthyThreshold,
path: targets.path,
pathMatchType: targets.pathMatchType,
rewritePath: targets.rewritePath,
+1 -3
View File
@@ -23,7 +23,6 @@ import m14 from "./scriptsPg/1.15.4";
import m15 from "./scriptsPg/1.16.0";
import m16 from "./scriptsPg/1.17.0";
import m17 from "./scriptsPg/1.18.0";
import m18 from "./scriptsPg/1.18.3";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -46,8 +45,7 @@ const migrations = [
{ version: "1.15.4", run: m14 },
{ version: "1.16.0", run: m15 },
{ version: "1.17.0", run: m16 },
{ version: "1.18.0", run: m17 },
{ version: "1.18.3", run: m18 }
{ version: "1.18.0", run: m17 }
// Add new migrations here as they are created
] as {
version: string;
+1 -3
View File
@@ -41,7 +41,6 @@ import m35 from "./scriptsSqlite/1.15.4";
import m36 from "./scriptsSqlite/1.16.0";
import m37 from "./scriptsSqlite/1.17.0";
import m38 from "./scriptsSqlite/1.18.0";
import m39 from "./scriptsSqlite/1.18.3";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -80,8 +79,7 @@ const migrations = [
{ version: "1.15.4", run: m35 },
{ version: "1.16.0", run: m36 },
{ version: "1.17.0", run: m37 },
{ version: "1.18.0", run: m38 },
{ version: "1.18.3", run: m39 }
{ version: "1.18.0", run: m38 }
// Add new migrations here as they are created
] as const;
-173
View File
@@ -1,173 +0,0 @@
import { db } from "@server/db/pg/driver";
import { sql } from "drizzle-orm";
const version = "1.18.3";
export default async function migration() {
console.log(`Running setup script ${version}...`);
// Query existing targetHealthCheck data with joined siteId and orgId before
// the transaction adds the new columns (which start NULL for existing rows).
// We will delete all rows and reinsert them with targetHealthCheckId = targetId
// so the two IDs form a stable 1:1 mapping.
const healthChecksQuery = await db.execute(
sql`SELECT
thc."targetHealthCheckId",
thc."targetId",
t."siteId",
s."orgId",
r."name" AS "resourceName",
t."ip",
t."port"
FROM "targetHealthCheck" thc
JOIN "targets" t ON thc."targetId" = t."targetId"
JOIN "sites" s ON t."siteId" = s."siteId"
JOIN "resources" r ON t."resourceId" = r."resourceId"
WHERE thc."name" IS NULL OR thc."name" = ''`
);
const existingHealthChecks = healthChecksQuery.rows as {
targetHealthCheckId: number;
targetId: number;
siteId: number;
orgId: string;
resourceName: string;
ip: string;
port: number;
}[];
console.log(
`Found ${existingHealthChecks.length} existing targetHealthCheck row(s) to migrate`
);
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`
CREATE TABLE "trialNotifications" (
"notificationId" serial PRIMARY KEY NOT NULL,
"subscriptionId" varchar(255) NOT NULL,
"notificationType" varchar(50) NOT NULL,
"sentAt" bigint NOT NULL
);
`);
await db.execute(sql`
ALTER TABLE "trialNotifications" ADD CONSTRAINT "trialNotifications_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`COMMIT`);
console.log("Migrated database");
} catch (e) {
await db.execute(sql`ROLLBACK`);
console.log("Unable to migrate database");
console.log(e);
throw e;
}
if (existingHealthChecks.length > 0) {
// fix the name column
try {
for (const hc of existingHealthChecks) {
await db.execute(sql`
UPDATE "targetHealthCheck"
SET "name" = ${`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`}
WHERE "targetHealthCheckId" = ${hc.targetHealthCheckId}
`);
}
console.log(
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
);
} catch (e) {
console.error("Error while migrating targetHealthCheck rows:", e);
throw e;
}
}
// Recompute resource health by aggregating across the resource's targets'
// target health checks, then update the resources.health column to match.
try {
const resourceTargetHealthQuery = await db.execute(
sql`SELECT
r."resourceId" AS "resourceId",
r."orgId" AS "orgId",
r."health" AS "currentHealth",
thc."hcHealth" AS "hcHealth"
FROM "resources" r
LEFT JOIN "targets" t ON t."resourceId" = r."resourceId"
LEFT JOIN "targetHealthCheck" thc ON thc."targetId" = t."targetId"`
);
const resourceTargetHealthRows = resourceTargetHealthQuery.rows as {
resourceId: number;
orgId: string;
currentHealth: string | null;
hcHealth: string | null;
}[];
const resourceHealthMap = new Map<
number,
{
hasHealthy: boolean;
hasUnhealthy: boolean;
hasUnknown: boolean;
orgId: string;
currentHealth: string | null;
}
>();
for (const row of resourceTargetHealthRows) {
const entry = resourceHealthMap.get(row.resourceId) ?? {
hasHealthy: false,
hasUnhealthy: false,
hasUnknown: false,
orgId: row.orgId,
currentHealth: row.currentHealth
};
const status = row.hcHealth ?? "unknown";
if (status === "healthy") entry.hasHealthy = true;
else if (status === "unhealthy") entry.hasUnhealthy = true;
else entry.hasUnknown = true;
resourceHealthMap.set(row.resourceId, entry);
}
const now = Math.floor(Date.now() / 1000);
let updatedResourceCount = 0;
for (const [resourceId, entry] of resourceHealthMap.entries()) {
let aggregated: "healthy" | "unhealthy" | "degraded" | "unknown";
if (entry.hasHealthy && entry.hasUnhealthy) {
aggregated = "degraded";
} else if (entry.hasHealthy) {
aggregated = "healthy";
} else if (entry.hasUnhealthy) {
aggregated = "unhealthy";
} else {
aggregated = "unknown";
}
if (entry.currentHealth !== aggregated) {
await db.execute(sql`
UPDATE "resources"
SET "health" = ${aggregated}
WHERE "resourceId" = ${resourceId}
`);
await db.execute(sql`
INSERT INTO "statusHistory" ("entityType", "entityId", "orgId", "status", "timestamp")
VALUES ('resource', ${resourceId}, ${entry.orgId}, ${aggregated}, ${now})
`);
updatedResourceCount++;
}
}
console.log(
`Recomputed health for ${updatedResourceCount} resource(s) based on target health checks`
);
} catch (e) {
console.error(
"Error while recomputing resource health from target health checks:",
e
);
throw e;
}
console.log(`${version} migration complete`);
}
-172
View File
@@ -1,172 +0,0 @@
import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.18.3";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.pragma("foreign_keys = OFF");
db.transaction(() => {
db.prepare(
`
CREATE TABLE 'trialNotifications' (
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'subscriptionId' text NOT NULL,
'notificationType' text NOT NULL,
'sentAt' integer NOT NULL,
FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade
);
`
).run();
})();
db.pragma("foreign_keys = ON");
console.log("Migrated database");
// Fix names for health checks that don't have one
const healthChecksWithoutName = db
.prepare(
`SELECT
thc."targetHealthCheckId",
r."name" AS "resourceName",
t."ip",
t."port"
FROM 'targetHealthCheck' thc
JOIN 'targets' t ON thc."targetId" = t."targetId"
JOIN 'resources' r ON t."resourceId" = r."resourceId"
WHERE thc."name" IS NULL OR thc."name" = ''`
)
.all() as {
targetHealthCheckId: number;
resourceName: string;
ip: string;
port: number;
}[];
console.log(
`Found ${healthChecksWithoutName.length} targetHealthCheck row(s) with missing names`
);
if (healthChecksWithoutName.length > 0) {
const updateName = db.prepare(
`UPDATE 'targetHealthCheck' SET "name" = ? WHERE "targetHealthCheckId" = ?`
);
const updateAllNames = db.transaction(() => {
for (const hc of healthChecksWithoutName) {
updateName.run(
`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`,
hc.targetHealthCheckId
);
}
});
updateAllNames();
console.log(
`Updated names for ${healthChecksWithoutName.length} targetHealthCheck row(s)`
);
}
// Recompute resource health by aggregating across the resource's
// targets' target health checks, then update resources.health and
// insert a statusHistory entry for any resource whose health changed.
const resourceTargetHealthRows = db
.prepare(
`SELECT
r."resourceId" AS "resourceId",
r."orgId" AS "orgId",
r."health" AS "currentHealth",
thc."hcHealth" AS "hcHealth"
FROM 'resources' r
LEFT JOIN 'targets' t ON t."resourceId" = r."resourceId"
LEFT JOIN 'targetHealthCheck' thc ON thc."targetId" = t."targetId"`
)
.all() as {
resourceId: number;
orgId: string;
currentHealth: string | null;
hcHealth: string | null;
}[];
const resourceHealthMap = new Map<
number,
{
hasHealthy: boolean;
hasUnhealthy: boolean;
hasUnknown: boolean;
orgId: string;
currentHealth: string | null;
}
>();
for (const row of resourceTargetHealthRows) {
const entry = resourceHealthMap.get(row.resourceId) ?? {
hasHealthy: false,
hasUnhealthy: false,
hasUnknown: false,
orgId: row.orgId,
currentHealth: row.currentHealth
};
const status = row.hcHealth ?? "unknown";
if (status === "healthy") entry.hasHealthy = true;
else if (status === "unhealthy") entry.hasUnhealthy = true;
else entry.hasUnknown = true;
resourceHealthMap.set(row.resourceId, entry);
}
const updateResourceHealth = db.prepare(
`UPDATE 'resources' SET "health" = ? WHERE "resourceId" = ?`
);
const insertResourceHistory = db.prepare(
`INSERT INTO 'statusHistory' ("entityType", "entityId", "orgId", "status", "timestamp") VALUES (?, ?, ?, ?, ?)`
);
const now = Math.floor(Date.now() / 1000);
let updatedResourceCount = 0;
const recomputeAll = db.transaction(() => {
for (const [resourceId, entry] of resourceHealthMap.entries()) {
let aggregated:
| "healthy"
| "unhealthy"
| "degraded"
| "unknown";
if (entry.hasHealthy && entry.hasUnhealthy) {
aggregated = "degraded";
} else if (entry.hasHealthy) {
aggregated = "healthy";
} else if (entry.hasUnhealthy) {
aggregated = "unhealthy";
} else {
aggregated = "unknown";
}
if (entry.currentHealth !== aggregated) {
updateResourceHealth.run(aggregated, resourceId);
insertResourceHistory.run(
"resource",
resourceId,
entry.orgId,
aggregated,
now
);
updatedResourceCount++;
}
}
});
recomputeAll();
console.log(
`Recomputed health for ${updatedResourceCount} resource(s) based on target health checks`
);
} catch (e) {
console.log("Failed to migrate db:", e);
throw e;
}
console.log(`${version} migration complete`);
}
@@ -35,7 +35,6 @@ import {
} from "@app/components/Credenza";
import { cn } from "@app/lib/cn";
import { CreditCard, ExternalLink, Check, AlertTriangle } from "lucide-react";
import { Badge } from "@app/components/ui/badge";
import { Alert, AlertTitle, AlertDescription } from "@app/components/ui/alert";
import {
Tooltip,
@@ -56,7 +55,6 @@ import {
tier3LimitSet
} from "@server/lib/billing/limitSet";
import { FeatureId } from "@server/lib/billing/features";
import TrialBillingBanner from "@app/components/TrialBillingBanner";
// Plan tier definitions matching the mockup
type PlanId = "basic" | "home" | "team" | "business" | "enterprise";
@@ -807,20 +805,6 @@ export default function BillingPage() {
return (
<SettingsContainer>
{/* Trial Banner */}
{isTrial && (
<TrialBillingBanner
onUpgrade={() => {
const currentPlan = planOptions.find(
(p) => p.id === currentPlanId
);
if (currentPlan?.tierType) {
handleStartSubscription(currentPlan.tierType);
}
}}
/>
)}
{/* Subscription Status Alert */}
{isProblematicState && statusMessage && (
<Alert variant="destructive" className="mb-6">
@@ -875,19 +859,8 @@ export default function BillingPage() {
)}
>
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-2xl">
{plan.name}
</span>
{isCurrentPlan && isTrial && (
<Badge
variant="outlinePrimary"
className="text-xs"
>
{t("billingTrialBadge") ||
"Free Trial"}
</Badge>
)}
<div className="text-2xl">
{plan.name}
</div>
<div className="mt-1">
<span className="text-xl">
@@ -652,8 +652,6 @@ function ProxyResourceTargetsForm({
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null,
hcHealthyThreshold: null,
hcUnhealthyThreshold: null,
siteType: sites.length > 0 ? sites[0].type : null,
new: true,
updated: false
@@ -763,9 +761,7 @@ function ProxyResourceTargetsForm({
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName,
hcHealthyThreshold: target.hcHealthyThreshold || null,
hcUnhealthyThreshold: target.hcUnhealthyThreshold || null
hcTlsServerName: target.hcTlsServerName
};
// Only include path-related fields for HTTP resources
@@ -1022,13 +1018,7 @@ function ProxyResourceTargetsForm({
30,
hcTlsServerName:
selectedTargetForHealthCheck.hcTlsServerName ||
undefined,
hcHealthyThreshold:
selectedTargetForHealthCheck.hcHealthyThreshold ||
1,
hcUnhealthyThreshold:
selectedTargetForHealthCheck.hcUnhealthyThreshold ||
1
undefined
}}
onChanges={async (config) => {
if (selectedTargetForHealthCheck) {
@@ -303,8 +303,6 @@ export default function Page() {
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null,
hcHealthyThreshold: null,
hcUnhealthyThreshold: null,
siteType: sites.length > 0 ? sites[0].type : null,
new: true,
updated: false
@@ -554,11 +552,7 @@ export default function Page() {
hcUnhealthyInterval:
target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName,
hcHealthyThreshold:
target.hcHealthyThreshold || null,
hcUnhealthyThreshold:
target.hcUnhealthyThreshold || null
hcTlsServerName: target.hcTlsServerName
};
// Only include path-related fields for HTTP resources
@@ -1526,13 +1520,7 @@ export default function Page() {
30,
hcTlsServerName:
selectedTargetForHealthCheck.hcTlsServerName ||
undefined,
hcHealthyThreshold:
selectedTargetForHealthCheck.hcHealthyThreshold ||
1,
hcUnhealthyThreshold:
selectedTargetForHealthCheck.hcUnhealthyThreshold ||
1
undefined
}}
onChanges={async (config) => {
if (selectedTargetForHealthCheck) {
+9 -13
View File
@@ -13,7 +13,6 @@ type DismissableBannerProps = {
titleIcon: ReactNode;
description: string;
children?: ReactNode;
dismissable?: boolean;
};
export const DismissableBanner = ({
@@ -22,8 +21,7 @@ export const DismissableBanner = ({
title,
titleIcon,
description,
children,
dismissable = true
children
}: DismissableBannerProps) => {
const [isDismissed, setIsDismissed] = useState(true);
const t = useTranslations();
@@ -68,21 +66,19 @@ export const DismissableBanner = ({
);
};
if (dismissable && isDismissed) {
if (isDismissed) {
return null;
}
return (
<Card className="mb-6 relative border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden">
{dismissable && (
<button
onClick={handleDismiss}
className="absolute top-3 right-3 z-10 p-1.5 rounded-md hover:bg-background/80 transition-colors cursor-pointer"
aria-label={t("dismiss")}
>
<X className="w-4 h-4 text-muted-foreground" />
</button>
)}
<button
onClick={handleDismiss}
className="absolute top-3 right-3 z-10 p-1.5 rounded-md hover:bg-background/80 transition-colors cursor-pointer"
aria-label={t("dismiss")}
>
<X className="w-4 h-4 text-muted-foreground" />
</button>
<CardContent className="p-6">
<div className="flex flex-col lg:flex-row lg:items-center gap-6">
<div className="flex-1 space-y-2 min-w-0">
+15 -89
View File
@@ -25,6 +25,7 @@ import {
import {
ArrowRight,
ArrowUpDown,
KeyRound,
MoreHorizontal
} from "lucide-react";
import { useMemo, useState } from "react";
@@ -49,7 +50,6 @@ import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "use-debounce";
import type { ListUserAdminOrgIdpsResponse } from "@server/routers/orgIdp/types";
import { cn } from "@app/lib/cn";
import { Badge } from "@app/components/ui/badge";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { isIdpGlobalModeBannerVisible } from "@app/components/IdpGlobalModeBanner";
@@ -63,61 +63,6 @@ export type IdpRow = {
type AdminIdpRow = ListUserAdminOrgIdpsResponse["idps"][number];
type ImportSourceOrg = { orgId: string; orgName: string };
type GroupedImportableIdp = {
idpId: number;
name: string;
type: string;
variant: string;
tags: string | null;
sources: ImportSourceOrg[];
};
function adminRowForImport(
group: GroupedImportableIdp,
source: ImportSourceOrg
): AdminIdpRow {
return {
idpId: group.idpId,
orgId: source.orgId,
orgName: source.orgName,
name: group.name,
type: group.type,
variant: group.variant,
tags: group.tags
};
}
function groupImportableIdps(rows: AdminIdpRow[]): GroupedImportableIdp[] {
const map = new Map<number, GroupedImportableIdp>();
for (const row of rows) {
let g = map.get(row.idpId);
if (!g) {
g = {
idpId: row.idpId,
name: row.name,
type: row.type,
variant: row.variant,
tags: row.tags,
sources: []
};
map.set(row.idpId, g);
}
if (!g.sources.some((s) => s.orgId === row.orgId)) {
g.sources.push({ orgId: row.orgId, orgName: row.orgName });
}
}
return Array.from(map.values())
.map((item) => ({
...item,
sources: [...item.sources].sort((a, b) =>
a.orgName.localeCompare(b.orgName)
)
}))
.sort((a, b) => b.name.localeCompare(a.name));
}
function IdpImportRowIcon({
type,
variant
@@ -169,22 +114,16 @@ export default function IdpTable({ idps, orgId }: Props) {
);
}, [adminIdpsRaw, orgId, idps]);
const importableGrouped = useMemo(
() => groupImportableIdps(importableIdps),
[importableIdps]
);
const shownImportGrouped = useMemo(() => {
const shownImportIdps = useMemo(() => {
const q = debouncedImportSearch.trim().toLowerCase();
if (!q) {
return importableGrouped;
return importableIdps;
}
return importableGrouped.filter((group) => {
const hay =
`${group.name} ${group.sources.map((s) => s.orgName).join(" ")}`.toLowerCase();
return importableIdps.filter((row) => {
const hay = `${row.orgName} ${row.name}`.toLowerCase();
return hay.includes(q);
});
}, [importableGrouped, debouncedImportSearch]);
}, [importableIdps, debouncedImportSearch]);
const deleteIdp = async (idpId: number) => {
try {
@@ -425,44 +364,31 @@ export default function IdpTable({ idps, orgId }: Props) {
{t("idpImportEmpty")}
</CommandEmpty>
<CommandGroup>
{shownImportGrouped.map((group) => (
{shownImportIdps.map((row) => (
<CommandItem
key={group.idpId}
key={`${row.idpId}:${row.orgId}`}
className="items-start gap-3 py-2.5"
value={`${group.idpId}:${group.name}:${group.sources.map((s) => s.orgName).join(" ")}`}
value={`${row.idpId}:${row.orgId}:${row.orgName}:${row.name}`}
disabled={!canImportOrgOidcIdp}
onSelect={() => {
if (!canImportOrgOidcIdp) {
return;
}
void importIdp(
adminRowForImport(
group,
group.sources[0]
)
);
void importIdp(row);
}}
>
<div className="mt-0.5 shrink-0">
<IdpImportRowIcon
type={group.type}
variant={group.variant}
type={row.type}
variant={row.variant}
/>
</div>
<div className="min-w-0 flex-1 text-left">
<div className="truncate font-medium leading-tight">
{group.name}
{row.orgName}
</div>
<div className="mt-1 flex flex-wrap gap-1">
{group.sources.map((src) => (
<Badge
key={src.orgId}
variant="secondary"
className="max-w-full truncate font-normal"
>
{src.orgName}
</Badge>
))}
<div className="truncate text-sm leading-tight text-muted-foreground">
{row.name}
</div>
</div>
</CommandItem>
+2 -2
View File
@@ -22,7 +22,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { LookupUserResponse } from "@server/routers/auth/lookupUser";
import { useTranslations } from "next-intl";
import LoginPasswordForm from "@app/components/LoginPasswordForm";
import SmartLoginOrgSelector from "@app/components/SmartLoginOrgSelector";
import LoginOrgSelector from "@app/components/LoginOrgSelector";
import UserProfileCard from "@app/components/UserProfileCard";
import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton";
import { Separator } from "@app/components/ui/separator";
@@ -206,7 +206,7 @@ export default function SmartLoginForm({
if (viewState.type === "orgSelector") {
return (
<div className="space-y-4">
<SmartLoginOrgSelector
<LoginOrgSelector
identifier={viewState.identifier}
lookupResult={viewState.lookupResult}
redirect={redirect}
-297
View File
@@ -1,297 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@app/components/ui/button";
import { Badge } from "@app/components/ui/badge";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useTranslations } from "next-intl";
import LoginPasswordForm from "@app/components/LoginPasswordForm";
import { LookupUserResponse } from "@server/routers/auth/lookupUser";
import UserProfileCard from "@app/components/UserProfileCard";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
import { generateOidcUrlProxy } from "@app/actions/server";
import {
redirect as redirectTo,
useRouter,
useSearchParams
} from "next/navigation";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Separator } from "@app/components/ui/separator";
type SmartLoginOrgSelectorProps = {
identifier: string;
lookupResult: LookupUserResponse;
redirect?: string;
forceLogin?: boolean;
onUseDifferentAccount?: () => void;
};
type OrgBucket = {
orgId: string;
orgName: string;
idps: Array<{
idpId: number;
name: string;
variant: string | null;
}>;
hasInternalAuth: boolean;
};
type GroupedLoginIdp = {
idpId: number;
name: string;
variant: string | null;
orgs: { orgId: string; orgName: string }[];
};
function buildOrgMap(lookupResult: LookupUserResponse) {
const orgMap = new Map<string, OrgBucket>();
for (const account of lookupResult.accounts) {
for (const org of account.orgs) {
if (!orgMap.has(org.orgId)) {
orgMap.set(org.orgId, {
orgId: org.orgId,
orgName: org.orgName,
idps: org.idps,
hasInternalAuth: org.hasInternalAuth
});
} else {
const existing = orgMap.get(org.orgId)!;
const existingIdpIds = new Set(
existing.idps.map((i) => i.idpId)
);
for (const idp of org.idps) {
if (!existingIdpIds.has(idp.idpId)) {
existing.idps.push(idp);
}
}
if (org.hasInternalAuth) {
existing.hasInternalAuth = true;
}
}
}
}
return Array.from(orgMap.values());
}
function groupIdpsAcrossOrgs(orgs: OrgBucket[]): GroupedLoginIdp[] {
const map = new Map<number, GroupedLoginIdp>();
for (const org of orgs) {
for (const idp of org.idps) {
let g = map.get(idp.idpId);
if (!g) {
g = {
idpId: idp.idpId,
name: idp.name,
variant: idp.variant,
orgs: []
};
map.set(idp.idpId, g);
}
if (!g.orgs.some((o) => o.orgId === org.orgId)) {
g.orgs.push({ orgId: org.orgId, orgName: org.orgName });
}
}
}
return Array.from(map.values())
.map((g) => ({
...g,
orgs: [...g.orgs].sort((a, b) => a.orgName.localeCompare(b.orgName))
}))
.sort((a, b) => b.name.localeCompare(a.name));
}
export default function SmartLoginOrgSelector({
identifier,
lookupResult,
redirect,
forceLogin,
onUseDifferentAccount
}: SmartLoginOrgSelectorProps) {
const t = useTranslations();
const [showPasswordForm, setShowPasswordForm] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pendingIdpId, setPendingIdpId] = useState<number | null>(null);
const params = useSearchParams();
const router = useRouter();
const orgs = buildOrgMap(lookupResult);
const groupedIdps = groupIdpsAcrossOrgs(orgs);
const hasInternalAccount = lookupResult.accounts.some(
(acc) => acc.hasInternalAuth
);
function goToApp() {
const url = window.location.href.split("?")[0];
router.push(url);
}
useEffect(() => {
if (params.get("gotoapp")) {
goToApp();
}
}, []);
async function loginWithIdp(idpId: number, orgId: string) {
setPendingIdpId(idpId);
setError(null);
let redirectToUrl: string | undefined;
try {
const safeRedirect = cleanRedirect(redirect || "/");
const response = await generateOidcUrlProxy(
idpId,
safeRedirect,
orgId,
forceLogin
);
if (response.error) {
setError(response.message);
setPendingIdpId(null);
return;
}
const data = response.data;
if (data?.redirectUrl) {
redirectToUrl = data.redirectUrl;
}
} catch {
setError(
t("loginError", {
defaultValue:
"An unexpected error occurred. Please try again."
})
);
}
if (redirectToUrl) {
redirectTo(redirectToUrl);
} else {
setPendingIdpId(null);
}
}
if (showPasswordForm) {
return (
<div className="space-y-4">
<UserProfileCard
identifier={identifier}
description={t("loginSelectAuthenticationMethod")}
onUseDifferentAccount={onUseDifferentAccount}
useDifferentAccountText={t(
"deviceLoginUseDifferentAccount"
)}
/>
<LoginPasswordForm
identifier={identifier}
redirect={redirect}
forceLogin={forceLogin}
/>
</div>
);
}
return (
<div>
<UserProfileCard
identifier={identifier}
description={t("loginSelectAuthenticationMethod")}
onUseDifferentAccount={onUseDifferentAccount}
useDifferentAccountText={t("deviceLoginUseDifferentAccount")}
/>
{hasInternalAccount && (
<div className="mt-3">
<Button
type="button"
className="w-full"
onClick={() => setShowPasswordForm(true)}
>
{t("signInWithPassword")}
</Button>
</div>
)}
{groupedIdps.length > 0 ? (
<div className="mt-3 space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<Separator />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="px-2 bg-card text-muted-foreground">
{t("idpContinue")}
</span>
</div>
</div>
<div className="space-y-2">
{params.get("gotoapp") ? (
<Button
type="button"
className="w-full"
onClick={() => {
goToApp();
}}
>
{t("continueToApplication")}
</Button>
) : (
groupedIdps.map((group) => {
const effectiveType =
group.variant || group.name.toLowerCase();
const sourceOrgId = group.orgs[0].orgId;
return (
<Button
key={group.idpId}
type="button"
variant="outline"
className="h-auto w-full flex flex-wrap items-center justify-start gap-x-2 gap-y-1.5 py-3 text-left"
onClick={() => {
void loginWithIdp(
group.idpId,
sourceOrgId
);
}}
disabled={pendingIdpId !== null}
>
<IdpTypeIcon
type={effectiveType}
size={16}
className="shrink-0"
/>
<span className="font-medium shrink-0">
{group.name}
</span>
{group.orgs.map((org) => (
<Badge
key={org.orgId}
variant="secondary"
className="max-w-full shrink-0 truncate font-normal"
>
{org.orgName}
</Badge>
))}
</Button>
);
})
)}
</div>
</div>
) : null}
</div>
);
}
-38
View File
@@ -1,38 +0,0 @@
"use client";
import React from "react";
import { Button } from "@app/components/ui/button";
import { ClockIcon, ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import DismissableBanner from "./DismissableBanner";
type TrialBillingBannerProps = {
onUpgrade: () => void;
};
export const TrialBillingBanner = ({ onUpgrade }: TrialBillingBannerProps) => {
const t = useTranslations();
return (
<DismissableBanner
storageKey="trial-billing-banner-dismissed"
version={1}
title={t("billingTrialBannerTitle")}
titleIcon={<ClockIcon className="w-5 h-5 text-primary" />}
description={t("billingTrialBannerDescription")}
dismissable={false}
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
onClick={onUpgrade}
>
{t("billingTrialBannerUpgrade")}
<ArrowRight className="w-4 h-4" />
</Button>
</DismissableBanner>
);
};
export default TrialBillingBanner;
+7 -16
View File
@@ -52,10 +52,6 @@ export function NewtSiteInstallCommands({
const acceptClientsEnv = !acceptClients
? "\n - DISABLE_CLIENTS=true"
: "";
const acceptClientsHelmValue = acceptClients
? ` \\
--set newtInstances[0].acceptClients=true`
: "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
linux: {
@@ -166,18 +162,13 @@ sudo systemctl enable --now newt`
"Helm Chart": [
`helm repo add fossorial https://charts.fossorial.io`,
`helm repo update fossorial`,
`kubectl create namespace newt --dry-run=client -o yaml | kubectl apply -f -`,
`kubectl create secret generic newt-main-tunnel-auth \\
-n newt \\
--from-literal=PANGOLIN_ENDPOINT="${endpoint}" \\
--from-literal=NEWT_ID="${id}" \\
--from-literal=NEWT_SECRET="${secret}" \\
--dry-run=client -o yaml | kubectl apply -f -`,
`helm upgrade --install newt fossorial/newt \\
-n newt \\
--set newtInstances[0].name="main-tunnel" \\
--set newtInstances[0].enabled=true \\
--set-string newtInstances[0].auth.existingSecretName="newt-main-tunnel-auth"${acceptClientsHelmValue}`
`helm install newt fossorial/newt \\
--create-namespace \\
--set newtInstances[0].name="main-tunnel" \\
--set newtInstances[0].enabled=true \\
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
]
},
podman: {