Compare commits

..

66 Commits

Author SHA1 Message Date
Owen Schwartz 2ca400ab16 New translations en-us.json (French) 2025-12-24 16:14:26 -05:00
Owen Schwartz 4183067c77 New translations en-us.json (Norwegian Bokmal) 2025-12-24 16:14:26 -05:00
Owen Schwartz 5eb4691973 New translations en-us.json (Chinese Simplified) 2025-12-24 16:14:26 -05:00
Owen Schwartz d14dfbf360 New translations en-us.json (Turkish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 493a5ad02a New translations en-us.json (Russian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 481beff028 New translations en-us.json (Portuguese) 2025-12-24 16:14:26 -05:00
Owen Schwartz f1f7e438b4 New translations en-us.json (Polish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 00f84c9d8e New translations en-us.json (Dutch) 2025-12-24 16:14:26 -05:00
Owen Schwartz f75b9c6c86 New translations en-us.json (Korean) 2025-12-24 16:14:26 -05:00
Owen Schwartz 31bc6d5773 New translations en-us.json (Italian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 51dc1450d3 New translations en-us.json (German) 2025-12-24 16:14:26 -05:00
Owen Schwartz fcbea08c87 New translations en-us.json (Czech) 2025-12-24 16:14:26 -05:00
Owen Schwartz 8d60a87aa1 New translations en-us.json (Bulgarian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 956aa64519 New translations en-us.json (Spanish) 2025-12-24 16:14:26 -05:00
Owen Schwartz fd1cb6ca23 New translations en-us.json (French) 2025-12-24 16:14:26 -05:00
Owen Schwartz 37082ae436 New translations en-us.json (Norwegian Bokmal) 2025-12-24 16:14:26 -05:00
Owen Schwartz bb47ca3d2e New translations en-us.json (Chinese Simplified) 2025-12-24 16:14:26 -05:00
Owen Schwartz 0dd3c84b24 New translations en-us.json (Turkish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 848fca7e1b New translations en-us.json (Russian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 2500f99722 New translations en-us.json (Portuguese) 2025-12-24 16:14:26 -05:00
Owen Schwartz c7737c444f New translations en-us.json (Polish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 4d1a7ed69b New translations en-us.json (Dutch) 2025-12-24 16:14:26 -05:00
Owen Schwartz 626d5df67e New translations en-us.json (Korean) 2025-12-24 16:14:26 -05:00
Owen Schwartz e4c369deec New translations en-us.json (Italian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 307209e73f New translations en-us.json (German) 2025-12-24 16:14:26 -05:00
Owen Schwartz dc84935ee6 New translations en-us.json (Czech) 2025-12-24 16:14:26 -05:00
Owen Schwartz 998c1f52ca New translations en-us.json (Bulgarian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 2766758c66 New translations en-us.json (Spanish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 258d1d82f3 New translations en-us.json (French) 2025-12-24 16:14:26 -05:00
Owen Schwartz 46aaadb76a New translations en-us.json (Norwegian Bokmal) 2025-12-24 16:14:26 -05:00
Owen Schwartz ea7a618810 New translations en-us.json (Chinese Simplified) 2025-12-24 16:14:26 -05:00
Owen Schwartz c0e503b31f New translations en-us.json (Turkish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 55f5a41752 New translations en-us.json (Russian) 2025-12-24 16:14:26 -05:00
Owen Schwartz b0be82be86 New translations en-us.json (Portuguese) 2025-12-24 16:14:26 -05:00
Owen Schwartz 96a9bdb700 New translations en-us.json (Polish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 74e6d39c24 New translations en-us.json (Dutch) 2025-12-24 16:14:26 -05:00
Owen Schwartz 61dfa00222 New translations en-us.json (Korean) 2025-12-24 16:14:26 -05:00
Owen Schwartz 476281db2b New translations en-us.json (Italian) 2025-12-24 16:14:26 -05:00
Owen Schwartz f32e31c73d New translations en-us.json (Czech) 2025-12-24 16:14:26 -05:00
Owen Schwartz ea72279080 New translations en-us.json (Bulgarian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 16ba56af84 New translations en-us.json (Spanish) 2025-12-24 16:14:26 -05:00
Owen Schwartz f13ddde988 New translations en-us.json (Norwegian Bokmal) 2025-12-24 16:14:26 -05:00
Owen Schwartz 67dc10dfe9 New translations en-us.json (Chinese Simplified) 2025-12-24 16:14:26 -05:00
Owen Schwartz 5fd216adc2 New translations en-us.json (Turkish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 6f0268f6c0 New translations en-us.json (Russian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 2996dfb33a New translations en-us.json (Portuguese) 2025-12-24 16:14:26 -05:00
Owen Schwartz c92f2cd4ba New translations en-us.json (Polish) 2025-12-24 16:14:26 -05:00
Owen Schwartz 8164d5c1ad New translations en-us.json (Dutch) 2025-12-24 16:14:26 -05:00
Owen Schwartz d9d8d85f6e New translations en-us.json (Korean) 2025-12-24 16:14:26 -05:00
Owen Schwartz d49720703f New translations en-us.json (Italian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 2362a9b4dd New translations en-us.json (German) 2025-12-24 16:14:26 -05:00
Owen Schwartz a8265a5286 New translations en-us.json (Czech) 2025-12-24 16:14:26 -05:00
Owen Schwartz 9ea7431b73 New translations en-us.json (Bulgarian) 2025-12-24 16:14:26 -05:00
Owen Schwartz 37e6f320fe New translations en-us.json (Spanish) 2025-12-24 16:14:26 -05:00
miloschwartz c0c0d48edf ui enhancements 2025-12-24 16:14:26 -05:00
Owen 284cccbe17 Attempt to fix loginPageOrg undefined error 2025-12-24 16:14:26 -05:00
Owen 81a9a94264 Try to remove deadlocks on client updates 2025-12-24 16:14:26 -05:00
Owen dccf101554 Allow all in country in blueprints
Fixes #2163
2025-12-24 16:14:26 -05:00
Owen a01c06bbc7 Respect http status for url & maintenance mode
Fixes #2164
2025-12-24 16:14:26 -05:00
miloschwartz db43cf1b30 add sticky actions col to org idp table 2025-12-24 16:14:26 -05:00
miloschwartz 2f561b5604 adjustments to mobile header css closes #1930 2025-12-24 16:14:26 -05:00
miloschwartz 5a30f036ff fade mobile footer 2025-12-24 16:14:26 -05:00
miloschwartz 768b9ffd09 fix server admin spacing on mobile sidebar 2025-12-24 16:14:26 -05:00
miloschwartz 8732e50047 add flag to disable product help banners 2025-12-24 16:14:26 -05:00
miloschwartz d6e0024c96 improved button loading animation 2025-12-24 16:14:26 -05:00
miloschwartz 9759e86921 add stripPortFromHost and reuse everywhere 2025-12-24 16:14:26 -05:00
45 changed files with 410 additions and 623 deletions
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Конфигуриране на достъп за организация", "orgPolicyConfig": "Конфигуриране на достъп за организация",
"idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно", "idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно",
"redirectUrl": "URL за пренасочване", "redirectUrl": "URL за пренасочване",
"orgIdpRedirectUrls": "URL адреси за пренасочване",
"redirectUrlAbout": "За URL за пренасочване", "redirectUrlAbout": "За URL за пренасочване",
"redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.", "redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.",
"pangolinAuth": "Authent - Pangolin", "pangolinAuth": "Authent - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Съгласен съм с", "IAgreeToThe": "Съгласен съм с",
"termsOfService": "условията за ползване", "termsOfService": "условията за ползване",
"and": "и", "and": "и",
"privacyPolicy": "политиката за поверителност" "privacyPolicy": "политика за поверителност."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл." "keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Въведете потвърждение.", "enterConfirmation": "Въведете потвърждение.",
"blueprintViewDetails": "Подробности.", "blueprintViewDetails": "Подробности.",
"defaultIdentityProvider": "По подразбиране доставчик на идентичност.", "defaultIdentityProvider": "По подразбиране доставчик на идентичност.",
"defaultIdentityProviderDescription": "Когато е избран основен доставчик на идентичност, потребителят ще бъде автоматично пренасочен към доставчика за удостоверяване.",
"editInternalResourceDialogNetworkSettings": "Мрежови настройки.", "editInternalResourceDialogNetworkSettings": "Мрежови настройки.",
"editInternalResourceDialogAccessPolicy": "Политика за достъп.", "editInternalResourceDialogAccessPolicy": "Политика за достъп.",
"editInternalResourceDialogAddRoles": "Добавяне на роли.", "editInternalResourceDialogAddRoles": "Добавяне на роли.",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Konfigurace přístupu pro organizaci", "orgPolicyConfig": "Konfigurace přístupu pro organizaci",
"idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován", "idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován",
"redirectUrl": "Přesměrovat URL", "redirectUrl": "Přesměrovat URL",
"orgIdpRedirectUrls": "Přesměrovat URL",
"redirectUrlAbout": "O přesměrování URL", "redirectUrlAbout": "O přesměrování URL",
"redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nastavit v nastavení poskytovatele identity.", "redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nastavit v nastavení poskytovatele identity.",
"pangolinAuth": "Auth - Pangolin", "pangolinAuth": "Auth - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Souhlasím s", "IAgreeToThe": "Souhlasím s",
"termsOfService": "podmínky služby", "termsOfService": "podmínky služby",
"and": "a", "and": "a",
"privacyPolicy": "zásady ochrany osobních údajů" "privacyPolicy": "zásady ochrany osobních údajů."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem." "keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Zadejte potvrzení", "enterConfirmation": "Zadejte potvrzení",
"blueprintViewDetails": "Detaily", "blueprintViewDetails": "Detaily",
"defaultIdentityProvider": "Výchozí poskytovatel identity", "defaultIdentityProvider": "Výchozí poskytovatel identity",
"defaultIdentityProviderDescription": "Pokud je vybrán výchozí poskytovatel identity, uživatel bude automaticky přesměrován na poskytovatele pro ověření.",
"editInternalResourceDialogNetworkSettings": "Nastavení sítě", "editInternalResourceDialogNetworkSettings": "Nastavení sítě",
"editInternalResourceDialogAccessPolicy": "Přístupová politika", "editInternalResourceDialogAccessPolicy": "Přístupová politika",
"editInternalResourceDialogAddRoles": "Přidat role", "editInternalResourceDialogAddRoles": "Přidat role",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Zugriff für eine Organisation konfigurieren", "orgPolicyConfig": "Zugriff für eine Organisation konfigurieren",
"idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert", "idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert",
"redirectUrl": "Weiterleitungs-URL", "redirectUrl": "Weiterleitungs-URL",
"orgIdpRedirectUrls": "Umleitungs-URLs",
"redirectUrlAbout": "Über die Weiterleitungs-URL", "redirectUrlAbout": "Über die Weiterleitungs-URL",
"redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung umgeleitet werden. Sie müssen diese URL in den Einstellungen des Identity Providers konfigurieren.", "redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung umgeleitet werden. Sie müssen diese URL in den Einstellungen des Identity Providers konfigurieren.",
"pangolinAuth": "Authentifizierung - Pangolin", "pangolinAuth": "Authentifizierung - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Ich stimme den", "IAgreeToThe": "Ich stimme den",
"termsOfService": "Nutzungsbedingungen zu", "termsOfService": "Nutzungsbedingungen zu",
"and": "und", "and": "und",
"privacyPolicy": "Datenschutzrichtlinie" "privacyPolicy": "datenschutzrichtlinie."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail." "keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Bestätigung eingeben", "enterConfirmation": "Bestätigung eingeben",
"blueprintViewDetails": "Details", "blueprintViewDetails": "Details",
"defaultIdentityProvider": "Standard Identitätsanbieter", "defaultIdentityProvider": "Standard Identitätsanbieter",
"defaultIdentityProviderDescription": "Wenn ein Standard-Identity Provider ausgewählt ist, wird der Benutzer zur Authentifizierung automatisch an den Anbieter weitergeleitet.",
"editInternalResourceDialogNetworkSettings": "Netzwerkeinstellungen", "editInternalResourceDialogNetworkSettings": "Netzwerkeinstellungen",
"editInternalResourceDialogAccessPolicy": "Zugriffsrichtlinie", "editInternalResourceDialogAccessPolicy": "Zugriffsrichtlinie",
"editInternalResourceDialogAddRoles": "Rollen hinzufügen", "editInternalResourceDialogAddRoles": "Rollen hinzufügen",
+1 -1
View File
@@ -1480,7 +1480,7 @@
"IAgreeToThe": "I agree to the", "IAgreeToThe": "I agree to the",
"termsOfService": "terms of service", "termsOfService": "terms of service",
"and": "and", "and": "and",
"privacyPolicy": "privacy policy" "privacyPolicy": "privacy policy."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email." "keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Configurar acceso para una organización", "orgPolicyConfig": "Configurar acceso para una organización",
"idpUpdatedDescription": "Proveedor de identidad actualizado correctamente", "idpUpdatedDescription": "Proveedor de identidad actualizado correctamente",
"redirectUrl": "URL de redirección", "redirectUrl": "URL de redirección",
"orgIdpRedirectUrls": "Redirigir URL",
"redirectUrlAbout": "Acerca de la URL de redirección", "redirectUrlAbout": "Acerca de la URL de redirección",
"redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración del proveedor de identidad.", "redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración del proveedor de identidad.",
"pangolinAuth": "Autenticación - Pangolin", "pangolinAuth": "Autenticación - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Estoy de acuerdo con los", "IAgreeToThe": "Estoy de acuerdo con los",
"termsOfService": "términos del servicio", "termsOfService": "términos del servicio",
"and": "y", "and": "y",
"privacyPolicy": "política de privacidad" "privacyPolicy": "política de privacidad."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico." "keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Ingresar confirmación", "enterConfirmation": "Ingresar confirmación",
"blueprintViewDetails": "Detalles", "blueprintViewDetails": "Detalles",
"defaultIdentityProvider": "Proveedor de identidad predeterminado", "defaultIdentityProvider": "Proveedor de identidad predeterminado",
"defaultIdentityProviderDescription": "Cuando se selecciona un proveedor de identidad por defecto, el usuario será redirigido automáticamente al proveedor de autenticación.",
"editInternalResourceDialogNetworkSettings": "Configuración de red", "editInternalResourceDialogNetworkSettings": "Configuración de red",
"editInternalResourceDialogAccessPolicy": "Política de acceso", "editInternalResourceDialogAccessPolicy": "Política de acceso",
"editInternalResourceDialogAddRoles": "Agregar roles", "editInternalResourceDialogAddRoles": "Agregar roles",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Configurer l'accès pour une organisation", "orgPolicyConfig": "Configurer l'accès pour une organisation",
"idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès", "idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès",
"redirectUrl": "URL de redirection", "redirectUrl": "URL de redirection",
"orgIdpRedirectUrls": "URL de redirection",
"redirectUrlAbout": "À propos de l'URL de redirection", "redirectUrlAbout": "À propos de l'URL de redirection",
"redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres du fournisseur d'identité.", "redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres du fournisseur d'identité.",
"pangolinAuth": "Auth - Pangolin", "pangolinAuth": "Auth - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Je suis d'accord avec", "IAgreeToThe": "Je suis d'accord avec",
"termsOfService": "les conditions d'utilisation", "termsOfService": "les conditions d'utilisation",
"and": "et", "and": "et",
"privacyPolicy": "la politique de confidentialité" "privacyPolicy": "politique de confidentialité."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Gardez-moi dans la boucle avec des nouvelles, des mises à jour et de nouvelles fonctionnalités par courriel." "keepMeInTheLoop": "Gardez-moi dans la boucle avec des nouvelles, des mises à jour et de nouvelles fonctionnalités par courriel."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Entrez la confirmation", "enterConfirmation": "Entrez la confirmation",
"blueprintViewDetails": "Détails", "blueprintViewDetails": "Détails",
"defaultIdentityProvider": "Fournisseur d'identité par défaut", "defaultIdentityProvider": "Fournisseur d'identité par défaut",
"defaultIdentityProviderDescription": "Lorsqu'un fournisseur d'identité par défaut est sélectionné, l'utilisateur sera automatiquement redirigé vers le fournisseur pour authentification.",
"editInternalResourceDialogNetworkSettings": "Paramètres réseau", "editInternalResourceDialogNetworkSettings": "Paramètres réseau",
"editInternalResourceDialogAccessPolicy": "Politique d'accès", "editInternalResourceDialogAccessPolicy": "Politique d'accès",
"editInternalResourceDialogAddRoles": "Ajouter des rôles", "editInternalResourceDialogAddRoles": "Ajouter des rôles",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Configura l'accesso per un'organizzazione", "orgPolicyConfig": "Configura l'accesso per un'organizzazione",
"idpUpdatedDescription": "Provider di identità aggiornato con successo", "idpUpdatedDescription": "Provider di identità aggiornato con successo",
"redirectUrl": "URL di Reindirizzamento", "redirectUrl": "URL di Reindirizzamento",
"orgIdpRedirectUrls": "Reindirizza URL",
"redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento", "redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento",
"redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti saranno reindirizzati dopo l'autenticazione. È necessario configurare questo URL nelle impostazioni del provider di identità.", "redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti saranno reindirizzati dopo l'autenticazione. È necessario configurare questo URL nelle impostazioni del provider di identità.",
"pangolinAuth": "Autenticazione - Pangolina", "pangolinAuth": "Autenticazione - Pangolina",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Accetto i", "IAgreeToThe": "Accetto i",
"termsOfService": "termini di servizio", "termsOfService": "termini di servizio",
"and": "e", "and": "e",
"privacyPolicy": "informativa sulla privacy" "privacyPolicy": "informativa sulla privacy."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail." "keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Inserisci conferma", "enterConfirmation": "Inserisci conferma",
"blueprintViewDetails": "Dettagli", "blueprintViewDetails": "Dettagli",
"defaultIdentityProvider": "Provider di Identità Predefinito", "defaultIdentityProvider": "Provider di Identità Predefinito",
"defaultIdentityProviderDescription": "Quando viene selezionato un provider di identità predefinito, l'utente verrà automaticamente reindirizzato al provider per l'autenticazione.",
"editInternalResourceDialogNetworkSettings": "Impostazioni di Rete", "editInternalResourceDialogNetworkSettings": "Impostazioni di Rete",
"editInternalResourceDialogAccessPolicy": "Politica di Accesso", "editInternalResourceDialogAccessPolicy": "Politica di Accesso",
"editInternalResourceDialogAddRoles": "Aggiungi Ruoli", "editInternalResourceDialogAddRoles": "Aggiungi Ruoli",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "조직에 대한 접근을 구성하십시오.", "orgPolicyConfig": "조직에 대한 접근을 구성하십시오.",
"idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다", "idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다",
"redirectUrl": "리디렉션 URL", "redirectUrl": "리디렉션 URL",
"orgIdpRedirectUrls": "리디렉션 URL",
"redirectUrlAbout": "리디렉션 URL에 대한 정보", "redirectUrlAbout": "리디렉션 URL에 대한 정보",
"redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.", "redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.",
"pangolinAuth": "인증 - 판골린", "pangolinAuth": "인증 - 판골린",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "동의합니다", "IAgreeToThe": "동의합니다",
"termsOfService": "서비스 약관", "termsOfService": "서비스 약관",
"and": "및", "and": "및",
"privacyPolicy": "개인 정보 보호 정책" "privacyPolicy": "개인 정보 보호 정책."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요." "keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "확인 입력", "enterConfirmation": "확인 입력",
"blueprintViewDetails": "세부 정보", "blueprintViewDetails": "세부 정보",
"defaultIdentityProvider": "기본 아이덴티티 공급자", "defaultIdentityProvider": "기본 아이덴티티 공급자",
"defaultIdentityProviderDescription": "기본 ID 공급자가 선택되면, 사용자는 인증을 위해 자동으로 해당 공급자로 리디렉션됩니다.",
"editInternalResourceDialogNetworkSettings": "네트워크 설정", "editInternalResourceDialogNetworkSettings": "네트워크 설정",
"editInternalResourceDialogAccessPolicy": "액세스 정책", "editInternalResourceDialogAccessPolicy": "액세스 정책",
"editInternalResourceDialogAddRoles": "역할 추가", "editInternalResourceDialogAddRoles": "역할 추가",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Konfigurer tilgang for en organisasjon", "orgPolicyConfig": "Konfigurer tilgang for en organisasjon",
"idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert", "idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert",
"redirectUrl": "Omdirigerings-URL", "redirectUrl": "Omdirigerings-URL",
"orgIdpRedirectUrls": "Omadressere URL'er",
"redirectUrlAbout": "Om omdirigerings-URL", "redirectUrlAbout": "Om omdirigerings-URL",
"redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.", "redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.",
"pangolinAuth": "Autentisering - Pangolin", "pangolinAuth": "Autentisering - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Jeg godtar", "IAgreeToThe": "Jeg godtar",
"termsOfService": "brukervilkårene", "termsOfService": "brukervilkårene",
"and": "og", "and": "og",
"privacyPolicy": "personvernerklæringen" "privacyPolicy": "retningslinjer for personvern"
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post." "keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Skriv inn bekreftelse", "enterConfirmation": "Skriv inn bekreftelse",
"blueprintViewDetails": "Detaljer", "blueprintViewDetails": "Detaljer",
"defaultIdentityProvider": "Standard identitetsleverandør", "defaultIdentityProvider": "Standard identitetsleverandør",
"defaultIdentityProviderDescription": "Når en standard identitetsleverandør er valgt, vil brukeren automatisk bli omdirigert til leverandøren for autentisering.",
"editInternalResourceDialogNetworkSettings": "Nettverksinnstillinger", "editInternalResourceDialogNetworkSettings": "Nettverksinnstillinger",
"editInternalResourceDialogAccessPolicy": "Tilgangsregler for tilgang", "editInternalResourceDialogAccessPolicy": "Tilgangsregler for tilgang",
"editInternalResourceDialogAddRoles": "Legg til roller", "editInternalResourceDialogAddRoles": "Legg til roller",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Toegang voor een organisatie configureren", "orgPolicyConfig": "Toegang voor een organisatie configureren",
"idpUpdatedDescription": "Identity provider succesvol bijgewerkt", "idpUpdatedDescription": "Identity provider succesvol bijgewerkt",
"redirectUrl": "Omleidings URL", "redirectUrl": "Omleidings URL",
"orgIdpRedirectUrls": "URL's omleiden",
"redirectUrlAbout": "Over omleidings-URL", "redirectUrlAbout": "Over omleidings-URL",
"redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in de instellingen van de identiteitsprovider.", "redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in de instellingen van de identiteitsprovider.",
"pangolinAuth": "Authenticatie - Pangolin", "pangolinAuth": "Authenticatie - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Ik ga akkoord met de", "IAgreeToThe": "Ik ga akkoord met de",
"termsOfService": "servicevoorwaarden", "termsOfService": "servicevoorwaarden",
"and": "en", "and": "en",
"privacyPolicy": "privacybeleid" "privacyPolicy": "privacy beleid"
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail." "keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Bevestiging invoeren", "enterConfirmation": "Bevestiging invoeren",
"blueprintViewDetails": "Details", "blueprintViewDetails": "Details",
"defaultIdentityProvider": "Standaard Identiteitsprovider", "defaultIdentityProvider": "Standaard Identiteitsprovider",
"defaultIdentityProviderDescription": "Wanneer een standaard identity provider is geselecteerd, zal de gebruiker automatisch worden doorgestuurd naar de provider voor authenticatie.",
"editInternalResourceDialogNetworkSettings": "Netwerkinstellingen", "editInternalResourceDialogNetworkSettings": "Netwerkinstellingen",
"editInternalResourceDialogAccessPolicy": "Toegangsbeleid", "editInternalResourceDialogAccessPolicy": "Toegangsbeleid",
"editInternalResourceDialogAddRoles": "Rollen toevoegen", "editInternalResourceDialogAddRoles": "Rollen toevoegen",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Skonfiguruj dostęp dla organizacji", "orgPolicyConfig": "Skonfiguruj dostęp dla organizacji",
"idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany", "idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany",
"redirectUrl": "URL przekierowania", "redirectUrl": "URL przekierowania",
"orgIdpRedirectUrls": "Przekieruj adresy URL",
"redirectUrlAbout": "O URL przekierowania", "redirectUrlAbout": "O URL przekierowania",
"redirectUrlAboutDescription": "Jest to adres URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten adres URL w ustawieniach dostawcy tożsamości.", "redirectUrlAboutDescription": "Jest to adres URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten adres URL w ustawieniach dostawcy tożsamości.",
"pangolinAuth": "Autoryzacja - Pangolin", "pangolinAuth": "Autoryzacja - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Zgadzam się z", "IAgreeToThe": "Zgadzam się z",
"termsOfService": "warunkami usługi", "termsOfService": "warunkami usługi",
"and": "oraz", "and": "oraz",
"privacyPolicy": "polityką prywatności" "privacyPolicy": "polityka prywatności."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail." "keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Wprowadź potwierdzenie", "enterConfirmation": "Wprowadź potwierdzenie",
"blueprintViewDetails": "Szczegóły", "blueprintViewDetails": "Szczegóły",
"defaultIdentityProvider": "Domyślny dostawca tożsamości", "defaultIdentityProvider": "Domyślny dostawca tożsamości",
"defaultIdentityProviderDescription": "Gdy zostanie wybrany domyślny dostawca tożsamości, użytkownik zostanie automatycznie przekierowany do dostawcy w celu uwierzytelnienia.",
"editInternalResourceDialogNetworkSettings": "Ustawienia sieci", "editInternalResourceDialogNetworkSettings": "Ustawienia sieci",
"editInternalResourceDialogAccessPolicy": "Polityka dostępowa", "editInternalResourceDialogAccessPolicy": "Polityka dostępowa",
"editInternalResourceDialogAddRoles": "Dodaj role", "editInternalResourceDialogAddRoles": "Dodaj role",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Configurar acesso para uma organização", "orgPolicyConfig": "Configurar acesso para uma organização",
"idpUpdatedDescription": "Provedor de identidade atualizado com sucesso", "idpUpdatedDescription": "Provedor de identidade atualizado com sucesso",
"redirectUrl": "URL de Redirecionamento", "redirectUrl": "URL de Redirecionamento",
"orgIdpRedirectUrls": "Redirecionar URLs",
"redirectUrlAbout": "Sobre o URL de Redirecionamento", "redirectUrlAbout": "Sobre o URL de Redirecionamento",
"redirectUrlAboutDescription": "Essa é a URL para a qual os usuários serão redirecionados após a autenticação. Você precisa configurar esta URL nas configurações do provedor de identidade.", "redirectUrlAboutDescription": "Essa é a URL para a qual os usuários serão redirecionados após a autenticação. Você precisa configurar esta URL nas configurações do provedor de identidade.",
"pangolinAuth": "Autenticação - Pangolin", "pangolinAuth": "Autenticação - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Concordo com", "IAgreeToThe": "Concordo com",
"termsOfService": "os termos de serviço", "termsOfService": "os termos de serviço",
"and": "e", "and": "e",
"privacyPolicy": "política de privacidade" "privacyPolicy": "política de privacidade."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail." "keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Inserir confirmação", "enterConfirmation": "Inserir confirmação",
"blueprintViewDetails": "Detalhes", "blueprintViewDetails": "Detalhes",
"defaultIdentityProvider": "Provedor de Identidade Padrão", "defaultIdentityProvider": "Provedor de Identidade Padrão",
"defaultIdentityProviderDescription": "Quando um provedor de identidade padrão for selecionado, o usuário será automaticamente redirecionado para o provedor de autenticação.",
"editInternalResourceDialogNetworkSettings": "Configurações de Rede", "editInternalResourceDialogNetworkSettings": "Configurações de Rede",
"editInternalResourceDialogAccessPolicy": "Política de Acesso", "editInternalResourceDialogAccessPolicy": "Política de Acesso",
"editInternalResourceDialogAddRoles": "Adicionar Funções", "editInternalResourceDialogAddRoles": "Adicionar Funções",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Настроить доступ для организации", "orgPolicyConfig": "Настроить доступ для организации",
"idpUpdatedDescription": "Поставщик удостоверений успешно обновлён", "idpUpdatedDescription": "Поставщик удостоверений успешно обновлён",
"redirectUrl": "URL редиректа", "redirectUrl": "URL редиректа",
"orgIdpRedirectUrls": "Перенаправление URL",
"redirectUrlAbout": "О редиректе URL", "redirectUrlAbout": "О редиректе URL",
"redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.", "redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.",
"pangolinAuth": "Аутентификация - Pangolin", "pangolinAuth": "Аутентификация - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Я согласен с", "IAgreeToThe": "Я согласен с",
"termsOfService": "условия использования", "termsOfService": "условия использования",
"and": "и", "and": "и",
"privacyPolicy": "политика конфиденциальности" "privacyPolicy": "политика конфиденциальности."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте." "keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Введите подтверждение", "enterConfirmation": "Введите подтверждение",
"blueprintViewDetails": "Подробности", "blueprintViewDetails": "Подробности",
"defaultIdentityProvider": "Поставщик удостоверений по умолчанию", "defaultIdentityProvider": "Поставщик удостоверений по умолчанию",
"defaultIdentityProviderDescription": "Когда выбран поставщик идентификации по умолчанию, пользователь будет автоматически перенаправлен на провайдер для аутентификации.",
"editInternalResourceDialogNetworkSettings": "Настройки сети", "editInternalResourceDialogNetworkSettings": "Настройки сети",
"editInternalResourceDialogAccessPolicy": "Политика доступа", "editInternalResourceDialogAccessPolicy": "Политика доступа",
"editInternalResourceDialogAddRoles": "Добавить роли", "editInternalResourceDialogAddRoles": "Добавить роли",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın", "orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın",
"idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi", "idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi",
"redirectUrl": "Yönlendirme URL'si", "redirectUrl": "Yönlendirme URL'si",
"orgIdpRedirectUrls": "Yönlendirme URL'leri",
"redirectUrlAbout": "Yönlendirme URL'si Hakkında", "redirectUrlAbout": "Yönlendirme URL'si Hakkında",
"redirectUrlAboutDescription": "Bu, kimlik doğrulamasından sonra kullanıcıların yönlendirileceği URL'dir. Bu URL'yi kimlik sağlayıcınızın ayarlarında yapılandırmanız gerekir.", "redirectUrlAboutDescription": "Bu, kimlik doğrulamasından sonra kullanıcıların yönlendirileceği URL'dir. Bu URL'yi kimlik sağlayıcınızın ayarlarında yapılandırmanız gerekir.",
"pangolinAuth": "Yetkilendirme - Pangolin", "pangolinAuth": "Yetkilendirme - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "Kabul ediyorum", "IAgreeToThe": "Kabul ediyorum",
"termsOfService": "hizmet şartları", "termsOfService": "hizmet şartları",
"and": "ve", "and": "ve",
"privacyPolicy": "gizlilik politikası" "privacyPolicy": "gizlilik politikası."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin." "keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin."
@@ -2349,6 +2350,7 @@
"enterConfirmation": "Onayı girin", "enterConfirmation": "Onayı girin",
"blueprintViewDetails": "Detaylar", "blueprintViewDetails": "Detaylar",
"defaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı", "defaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı",
"defaultIdentityProviderDescription": "Varsayılan bir kimlik sağlayıcı seçildiğinde, kullanıcı kimlik doğrulaması için otomatik olarak sağlayıcıya yönlendirilecektir.",
"editInternalResourceDialogNetworkSettings": "Ağ Ayarları", "editInternalResourceDialogNetworkSettings": "Ağ Ayarları",
"editInternalResourceDialogAccessPolicy": "Erişim Politikası", "editInternalResourceDialogAccessPolicy": "Erişim Politikası",
"editInternalResourceDialogAddRoles": "Roller Ekle", "editInternalResourceDialogAddRoles": "Roller Ekle",
+3 -1
View File
@@ -850,6 +850,7 @@
"orgPolicyConfig": "配置组织访问权限", "orgPolicyConfig": "配置组织访问权限",
"idpUpdatedDescription": "身份提供商更新成功", "idpUpdatedDescription": "身份提供商更新成功",
"redirectUrl": "重定向网址", "redirectUrl": "重定向网址",
"orgIdpRedirectUrls": "重定向URL",
"redirectUrlAbout": "关于重定向网址", "redirectUrlAbout": "关于重定向网址",
"redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。", "redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。",
"pangolinAuth": "认证 - Pangolin", "pangolinAuth": "认证 - Pangolin",
@@ -1479,7 +1480,7 @@
"IAgreeToThe": "我同意", "IAgreeToThe": "我同意",
"termsOfService": "服务条款", "termsOfService": "服务条款",
"and": "和", "and": "和",
"privacyPolicy": "隐私政策" "privacyPolicy": "隐私政策"
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。" "keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。"
@@ -2349,6 +2350,7 @@
"enterConfirmation": "输入确认", "enterConfirmation": "输入确认",
"blueprintViewDetails": "详细信息", "blueprintViewDetails": "详细信息",
"defaultIdentityProvider": "默认身份提供商", "defaultIdentityProvider": "默认身份提供商",
"defaultIdentityProviderDescription": "当选择默认身份提供商时,用户将自动重定向到提供商进行身份验证。",
"editInternalResourceDialogNetworkSettings": "网络设置", "editInternalResourceDialogNetworkSettings": "网络设置",
"editInternalResourceDialogAccessPolicy": "访问策略", "editInternalResourceDialogAccessPolicy": "访问策略",
"editInternalResourceDialogAddRoles": "添加角色", "editInternalResourceDialogAddRoles": "添加角色",
+6 -8
View File
@@ -111,32 +111,30 @@ export const RuleSchema = z
.refine( .refine(
(rule) => { (rule) => {
if (rule.match === "country") { if (rule.match === "country") {
// Check if it's a valid 2-letter country code // Check if it's a valid 2-letter country code or "ALL"
return /^[A-Z]{2}$/.test(rule.value); return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
} }
return true; return true;
}, },
{ {
path: ["value"], path: ["value"],
message: message:
"Value must be a 2-letter country code when match is 'country'" "Value must be a 2-letter country code or 'ALL' when match is 'country'"
} }
) )
.refine( .refine(
(rule) => { (rule) => {
if (rule.match === "asn") { if (rule.match === "asn") {
// Check if it's either AS<number> format or just a number // Check if it's either AS<number> format or "ALL"
const asNumberPattern = /^AS\d+$/i; const asNumberPattern = /^AS\d+$/i;
const isASFormat = asNumberPattern.test(rule.value); return asNumberPattern.test(rule.value) || rule.value === "ALL";
const isNumeric = /^\d+$/.test(rule.value);
return isASFormat || isNumeric;
} }
return true; return true;
}, },
{ {
path: ["value"], path: ["value"],
message: message:
"Value must be either 'AS<number>' format or a number when match is 'asn'" "Value must be 'AS<number>' format or 'ALL' when match is 'asn'"
} }
); );
-14
View File
@@ -573,20 +573,6 @@ class RedisManager {
} }
} }
public async incr(key: string): Promise<number> {
if (!this.isRedisEnabled() || !this.writeClient) return 0;
try {
return await this.executeWithRetry(
() => this.writeClient!.incr(key),
"Redis INCR"
);
} catch (error) {
logger.error("Redis INCR error:", error);
return 0;
}
}
public async sadd(key: string, member: string): Promise<boolean> { public async sadd(key: string, member: string): Promise<boolean> {
if (!this.isRedisEnabled() || !this.writeClient) return false; if (!this.isRedisEnabled() || !this.writeClient) return false;
+10 -10
View File
@@ -618,6 +618,16 @@ hybridRouter.get(
) )
.limit(1); .limit(1);
if (!result) {
return response<LoginPage | null>(res, {
data: null,
success: true,
error: false,
message: "Login page not found",
status: HttpCode.OK
});
}
if ( if (
await checkExitNodeOrg( await checkExitNodeOrg(
remoteExitNode.exitNodeId, remoteExitNode.exitNodeId,
@@ -633,16 +643,6 @@ hybridRouter.get(
); );
} }
if (!result) {
return response<LoginPage | null>(res, {
data: null,
success: true,
error: false,
message: "Login page not found",
status: HttpCode.OK
});
}
return response<LoginPage>(res, { return response<LoginPage>(res, {
data: result.loginPage, data: result.loginPage,
success: true, success: true,
@@ -40,6 +40,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
eq(loginPage.loginPageId, loginPageOrg.loginPageId) eq(loginPage.loginPageId, loginPageOrg.loginPageId)
) )
.limit(1); .limit(1);
if (!res) {
return null;
}
return { return {
...res.loginPage, ...res.loginPage,
orgId: res.loginPageOrg.orgId orgId: res.loginPageOrg.orgId
@@ -65,6 +70,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
) )
) )
.limit(1); .limit(1);
if (!res) {
return null;
}
return { return {
...res, ...res,
orgId: orgLink.orgId orgId: orgLink.orgId
@@ -48,6 +48,11 @@ async function query(orgId: string) {
) )
) )
.limit(1); .limit(1);
if (!res) {
return null;
}
return { return {
...res, ...res,
orgId: orgLink.orgs.orgId, orgId: orgLink.orgs.orgId,
+16 -110
View File
@@ -43,8 +43,7 @@ import {
WSMessage, WSMessage,
TokenPayload, TokenPayload,
WebSocketRequest, WebSocketRequest,
RedisMessage, RedisMessage
SendMessageOptions
} from "@server/routers/ws"; } from "@server/routers/ws";
import { validateSessionToken } from "@server/auth/sessions/app"; import { validateSessionToken } from "@server/auth/sessions/app";
@@ -173,9 +172,6 @@ const REDIS_CHANNEL = "websocket_messages";
// Client tracking map (local to this node) // Client tracking map (local to this node)
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map(); const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
// Config version tracking map (local to this node, resets on server restart)
const clientConfigVersions: Map<string, number> = new Map();
// Recovery tracking // Recovery tracking
let isRedisRecoveryInProgress = false; let isRedisRecoveryInProgress = false;
@@ -186,7 +182,6 @@ const getClientMapKey = (clientId: string) => clientId;
const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`; const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`;
const getNodeConnectionsKey = (nodeId: string, clientId: string) => const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
`ws:node:${nodeId}:${clientId}`; `ws:node:${nodeId}:${clientId}`;
const getConfigVersionKey = (clientId: string) => `ws:configVersion:${clientId}`;
// Initialize Redis subscription for cross-node messaging // Initialize Redis subscription for cross-node messaging
const initializeRedisSubscription = async (): Promise<void> => { const initializeRedisSubscription = async (): Promise<void> => {
@@ -382,76 +377,17 @@ const removeClient = async (
} }
}; };
// Helper to get the current config version for a client
const getClientConfigVersion = async (clientId: string): Promise<number> => {
// Try Redis first if available
if (redisManager.isRedisEnabled()) {
try {
const redisVersion = await redisManager.get(getConfigVersionKey(clientId));
if (redisVersion !== null) {
const version = parseInt(redisVersion, 10);
// Sync local cache with Redis
clientConfigVersions.set(clientId, version);
return version;
}
} catch (error) {
logger.error("Failed to get config version from Redis:", error);
}
}
// Fall back to local cache
return clientConfigVersions.get(clientId) || 0;
};
// Helper to increment and get the new config version for a client
const incrementClientConfigVersion = async (clientId: string): Promise<number> => {
let newVersion: number;
if (redisManager.isRedisEnabled()) {
try {
// Use Redis INCR for atomic increment across nodes
newVersion = await redisManager.incr(getConfigVersionKey(clientId));
// Sync local cache
clientConfigVersions.set(clientId, newVersion);
return newVersion;
} catch (error) {
logger.error("Failed to increment config version in Redis:", error);
// Fall through to local increment
}
}
// Local increment
const currentVersion = clientConfigVersions.get(clientId) || 0;
newVersion = currentVersion + 1;
clientConfigVersions.set(clientId, newVersion);
return newVersion;
};
// Local message sending (within this node) // Local message sending (within this node)
const sendToClientLocal = async ( const sendToClientLocal = async (
clientId: string, clientId: string,
message: WSMessage, message: WSMessage
options: SendMessageOptions = {}
): Promise<boolean> => { ): Promise<boolean> => {
const mapKey = getClientMapKey(clientId); const mapKey = getClientMapKey(clientId);
const clients = connectedClients.get(mapKey); const clients = connectedClients.get(mapKey);
if (!clients || clients.length === 0) { if (!clients || clients.length === 0) {
return false; return false;
} }
const messageString = JSON.stringify(message);
// Handle config version
let configVersion = await getClientConfigVersion(clientId);
if (options.incrementConfigVersion) {
configVersion = await incrementClientConfigVersion(clientId);
}
// Add config version to message
const messageWithVersion = {
...message,
configVersion
};
const messageString = JSON.stringify(messageWithVersion);
clients.forEach((client) => { clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(messageString); client.send(messageString);
@@ -459,69 +395,43 @@ const sendToClientLocal = async (
}); });
logger.debug( logger.debug(
`sendToClient: Message type ${message.type} sent to clientId ${clientId} (configVersion: ${configVersion})` `sendToClient: Message type ${message.type} sent to clientId ${clientId}`
); );
return true; return true;
}; };
const broadcastToAllExceptLocal = async ( const broadcastToAllExceptLocal = async (
message: WSMessage, message: WSMessage,
excludeClientId?: string, excludeClientId?: string
options: SendMessageOptions = {}
): Promise<void> => { ): Promise<void> => {
for (const [mapKey, clients] of connectedClients.entries()) { connectedClients.forEach((clients, mapKey) => {
const [type, id] = mapKey.split(":"); const [type, id] = mapKey.split(":");
const clientId = mapKey; // mapKey is the clientId if (!(excludeClientId && id === excludeClientId)) {
if (!(excludeClientId && clientId === excludeClientId)) {
// Handle config version per client
let configVersion = await getClientConfigVersion(clientId);
if (options.incrementConfigVersion) {
configVersion = await incrementClientConfigVersion(clientId);
}
// Add config version to message
const messageWithVersion = {
...message,
configVersion
};
clients.forEach((client) => { clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(messageWithVersion)); client.send(JSON.stringify(message));
} }
}); });
} }
} });
}; };
// Cross-node message sending (via Redis) // Cross-node message sending (via Redis)
const sendToClient = async ( const sendToClient = async (
clientId: string, clientId: string,
message: WSMessage, message: WSMessage
options: SendMessageOptions = {}
): Promise<boolean> => { ): Promise<boolean> => {
// Try to send locally first // Try to send locally first
const localSent = await sendToClientLocal(clientId, message, options); const localSent = await sendToClientLocal(clientId, message);
// Only send via Redis if the client is not connected locally and Redis is enabled // Only send via Redis if the client is not connected locally and Redis is enabled
if (!localSent && redisManager.isRedisEnabled()) { if (!localSent && redisManager.isRedisEnabled()) {
try { try {
// If we need to increment config version, do it before sending via Redis
// so remote nodes send the correct version
let configVersion = await getClientConfigVersion(clientId);
if (options.incrementConfigVersion) {
configVersion = await incrementClientConfigVersion(clientId);
}
const redisMessage: RedisMessage = { const redisMessage: RedisMessage = {
type: "direct", type: "direct",
targetClientId: clientId, targetClientId: clientId,
message: { message,
...message,
configVersion
},
fromNodeId: NODE_ID fromNodeId: NODE_ID
}; };
@@ -548,22 +458,19 @@ const sendToClient = async (
const broadcastToAllExcept = async ( const broadcastToAllExcept = async (
message: WSMessage, message: WSMessage,
excludeClientId?: string, excludeClientId?: string
options: SendMessageOptions = {}
): Promise<void> => { ): Promise<void> => {
// Broadcast locally // Broadcast locally
await broadcastToAllExceptLocal(message, excludeClientId, options); await broadcastToAllExceptLocal(message, excludeClientId);
// If Redis is enabled, also broadcast via Redis pub/sub to other nodes // If Redis is enabled, also broadcast via Redis pub/sub to other nodes
// Note: For broadcasts, we include the options so remote nodes can handle versioning
if (redisManager.isRedisEnabled()) { if (redisManager.isRedisEnabled()) {
try { try {
const redisMessage: RedisMessage = { const redisMessage: RedisMessage = {
type: "broadcast", type: "broadcast",
excludeClientId, excludeClientId,
message, message,
fromNodeId: NODE_ID, fromNodeId: NODE_ID
options
}; };
await redisManager.publish( await redisManager.publish(
@@ -1029,6 +936,5 @@ export {
getActiveNodes, getActiveNodes,
disconnectClient, disconnectClient,
NODE_ID, NODE_ID,
cleanup, cleanup
getClientConfigVersion
}; };
+1 -1
View File
@@ -36,7 +36,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.select() .select()
.from(clients) .from(clients)
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId))) .where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
.leftJoin(olms, eq(olms.clientId, olms.clientId)) .leftJoin(olms, eq(clients.clientId, olms.clientId))
.limit(1); .limit(1);
return res; return res;
} }
+17 -36
View File
@@ -7,8 +7,7 @@ import {
ExitNode, ExitNode,
exitNodes, exitNodes,
siteResources, siteResources,
clientSiteResourcesAssociationsCache, clientSiteResourcesAssociationsCache
Site
} from "@server/db"; } from "@server/db";
import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db"; import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -131,38 +130,6 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
} }
} }
const { peers, targets } = await buildClientConfigurationForNewtClient(
site,
exitNode
);
// Build the configuration response
const configResponse = {
ipAddress: site.address,
peers,
targets
};
logger.debug("Sending config: ", configResponse);
return {
message: {
type: "newt/wg/receive-config",
data: {
...configResponse
}
},
broadcast: false,
excludeSender: false,
};
};
export async function buildClientConfigurationForNewtClient(
site: Site,
exitNode?: ExitNode
) {
const siteId = site.siteId;
// Get all clients connected to this site // Get all clients connected to this site
const clientsRes = await db const clientsRes = await db
.select() .select()
@@ -311,8 +278,22 @@ export async function buildClientConfigurationForNewtClient(
targetsToSend.push(...resourceTargets); targetsToSend.push(...resourceTargets);
} }
return { // Build the configuration response
const configResponse = {
ipAddress: site.address,
peers: validPeers, peers: validPeers,
targets: targetsToSend targets: targetsToSend
}; };
}
logger.debug("Sending config: ", configResponse);
return {
message: {
type: "newt/wg/receive-config",
data: {
...configResponse
}
},
broadcast: false,
excludeSender: false
};
};
@@ -1,141 +0,0 @@
import { db } from "@server/db";
import { disconnectClient } from "#dynamic/routers/ws";
import { getClientConfigVersion, MessageHandler } from "@server/routers/ws";
import { clients, Newt } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
import logger from "@server/logger";
import { validateSessionToken } from "@server/auth/sessions/app";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { sendTerminateClient } from "../client/terminate";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
// Track if the offline checker interval is running
// let offlineCheckerInterval: NodeJS.Timeout | null = null;
// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
/**
* Starts the background interval that checks for clients that haven't pinged recently
* and marks them as offline
*/
// export const startNewtOfflineChecker = (): void => {
// if (offlineCheckerInterval) {
// return; // Already running
// }
// offlineCheckerInterval = setInterval(async () => {
// try {
// const twoMinutesAgo = Math.floor(
// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000
// );
// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
// // Find clients that haven't pinged in the last 2 minutes and mark them as offline
// const offlineClients = await db
// .update(clients)
// .set({ online: false })
// .where(
// and(
// eq(clients.online, true),
// or(
// lt(clients.lastPing, twoMinutesAgo),
// isNull(clients.lastPing)
// )
// )
// )
// .returning();
// for (const offlineClient of offlineClients) {
// logger.info(
// `Kicking offline newt client ${offlineClient.clientId} due to inactivity`
// );
// if (!offlineClient.newtId) {
// logger.warn(
// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect`
// );
// continue;
// }
// // Send a disconnect message to the client if connected
// try {
// await sendTerminateClient(
// offlineClient.clientId,
// offlineClient.newtId
// ); // terminate first
// // wait a moment to ensure the message is sent
// await new Promise((resolve) => setTimeout(resolve, 1000));
// await disconnectClient(offlineClient.newtId);
// } catch (error) {
// logger.error(
// `Error sending disconnect to offline newt ${offlineClient.clientId}`,
// { error }
// );
// }
// }
// } catch (error) {
// logger.error("Error in offline checker interval", { error });
// }
// }, OFFLINE_CHECK_INTERVAL);
// logger.debug("Started offline checker interval");
// };
/**
* Stops the background interval that checks for offline clients
*/
// export const stopNewtOfflineChecker = (): void => {
// if (offlineCheckerInterval) {
// clearInterval(offlineCheckerInterval);
// offlineCheckerInterval = null;
// logger.info("Stopped offline checker interval");
// }
// };
/**
* Handles ping messages from clients and responds with pong
*/
export const handleNewtPingMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const newt = c as Newt;
if (!newt) {
logger.warn("Newt not found");
return;
}
// get the version
const configVersion = await getClientConfigVersion(newt.newtId);
if (message.configVersion && configVersion != message.configVersion) {
logger.warn(`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`);
// TODO: sync the client
}
// try {
// // Update the client's last ping timestamp
// await db
// .update(clients)
// .set({
// lastPing: Math.floor(Date.now() / 1000),
// online: true
// })
// .where(eq(clients.clientId, newt.clientId));
// } catch (error) {
// logger.error("Error handling ping message", { error });
// }
return {
message: {
type: "pong",
data: {
timestamp: new Date().toISOString()
}
},
broadcast: false,
excludeSender: false
};
};
@@ -233,35 +233,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
.where(eq(newts.newtId, newt.newtId)); .where(eq(newts.newtId, newt.newtId));
} }
const { tcpTargets, udpTargets, validHealthCheckTargets } =
await buildTargetConfigurationForNewtClient(siteId);
logger.debug(
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
);
return {
message: {
type: "newt/wg/connect",
data: {
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
relayPort: config.getRawConfig().gerbil.clients_start_port,
publicKey: exitNode.publicKey,
serverIP: exitNode.address.split("/")[0],
tunnelIP: siteSubnet.split("/")[0],
targets: {
udp: udpTargets,
tcp: tcpTargets
},
healthCheckTargets: validHealthCheckTargets
}
},
broadcast: false, // Send to all clients
excludeSender: false // Include sender in broadcast
};
};
export async function buildTargetConfigurationForNewtClient(siteId: number) {
// Get all enabled targets with their resource protocol information // Get all enabled targets with their resource protocol information
const allTargets = await db const allTargets = await db
.select({ .select({
@@ -366,12 +337,30 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
(target) => target !== null (target) => target !== null
); );
logger.debug(
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
);
return { return {
validHealthCheckTargets, message: {
tcpTargets, type: "newt/wg/connect",
udpTargets data: {
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
relayPort: config.getRawConfig().gerbil.clients_start_port,
publicKey: exitNode.publicKey,
serverIP: exitNode.address.split("/")[0],
tunnelIP: siteSubnet.split("/")[0],
targets: {
udp: udpTargets,
tcp: tcpTargets
},
healthCheckTargets: validHealthCheckTargets
}
},
broadcast: false, // Send to all clients
excludeSender: false // Include sender in broadcast
}; };
} };
async function getUniqueSubnetForSite( async function getUniqueSubnetForSite(
exitNode: ExitNode, exitNode: ExitNode,
@@ -1,7 +1,7 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { MessageHandler } from "@server/routers/ws"; import { MessageHandler } from "@server/routers/ws";
import { clients, Newt } from "@server/db"; import { clients } from "@server/db";
import { eq } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
interface PeerBandwidth { interface PeerBandwidth {
@@ -10,13 +10,57 @@ interface PeerBandwidth {
bytesOut: number; bytesOut: number;
} }
// Retry configuration for deadlock handling
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 50;
/**
* Check if an error is a deadlock error
*/
function isDeadlockError(error: any): boolean {
return (
error?.code === "40P01" ||
error?.cause?.code === "40P01" ||
(error?.message && error.message.includes("deadlock"))
);
}
/**
* Execute a function with retry logic for deadlock handling
*/
async function withDeadlockRetry<T>(
operation: () => Promise<T>,
context: string
): Promise<T> {
let attempt = 0;
while (true) {
try {
return await operation();
} catch (error: any) {
if (isDeadlockError(error) && attempt < MAX_RETRIES) {
attempt++;
const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS;
const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter;
logger.warn(
`Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
export const handleReceiveBandwidthMessage: MessageHandler = async ( export const handleReceiveBandwidthMessage: MessageHandler = async (
context context
) => { ) => {
const { message, client, sendToClient } = context; const { message } = context;
if (!message.data.bandwidthData) { if (!message.data.bandwidthData) {
logger.warn("No bandwidth data provided"); logger.warn("No bandwidth data provided");
return;
} }
const bandwidthData: PeerBandwidth[] = message.data.bandwidthData; const bandwidthData: PeerBandwidth[] = message.data.bandwidthData;
@@ -25,30 +69,40 @@ export const handleReceiveBandwidthMessage: MessageHandler = async (
throw new Error("Invalid bandwidth data"); throw new Error("Invalid bandwidth data");
} }
await db.transaction(async (trx) => { // Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances
for (const peer of bandwidthData) { // This is critical for preventing deadlocks when multiple instances update the same clients
const { publicKey, bytesIn, bytesOut } = peer; const sortedBandwidthData = [...bandwidthData].sort((a, b) =>
a.publicKey.localeCompare(b.publicKey)
);
// Find the client by public key const currentTime = new Date().toISOString();
const [client] = await trx
.select()
.from(clients)
.where(eq(clients.pubKey, publicKey))
.limit(1);
if (!client) { // Update each client individually with retry logic
continue; // This reduces transaction scope and allows retries per-client
} for (const peer of sortedBandwidthData) {
const { publicKey, bytesIn, bytesOut } = peer;
// Update the client's bandwidth usage try {
await trx await withDeadlockRetry(async () => {
.update(clients) // Use atomic SQL increment to avoid SELECT then UPDATE pattern
.set({ // This eliminates the need to read the current value first
megabytesOut: (client.megabytesIn || 0) + bytesIn, await db
megabytesIn: (client.megabytesOut || 0) + bytesOut, .update(clients)
lastBandwidthUpdate: new Date().toISOString() .set({
}) // Note: bytesIn from peer goes to megabytesOut (data sent to client)
.where(eq(clients.clientId, client.clientId)); // and bytesOut from peer goes to megabytesIn (data received from client)
megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`,
megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`,
lastBandwidthUpdate: currentTime
})
.where(eq(clients.pubKey, publicKey));
}, `update client bandwidth ${publicKey}`);
} catch (error) {
logger.error(
`Failed to update bandwidth for client ${publicKey}:`,
error
);
// Continue with other clients even if one fails
} }
}); }
}; };
-1
View File
@@ -6,4 +6,3 @@ export * from "./handleGetConfigMessage";
export * from "./handleSocketMessages"; export * from "./handleSocketMessages";
export * from "./handleNewtPingRequestMessage"; export * from "./handleNewtPingRequestMessage";
export * from "./handleApplyBlueprintMessage"; export * from "./handleApplyBlueprintMessage";
export * from "./handleNewtPingMessage";
+1 -10
View File
@@ -1,6 +1,6 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { disconnectClient } from "#dynamic/routers/ws"; import { disconnectClient } from "#dynamic/routers/ws";
import { getClientConfigVersion, MessageHandler } from "@server/routers/ws"; import { MessageHandler } from "@server/routers/ws";
import { clients, Olm } from "@server/db"; import { clients, Olm } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm"; import { eq, lt, isNull, and, or } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
@@ -108,15 +108,6 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
return; return;
} }
// get the version
const configVersion = await getClientConfigVersion(olm.olmId);
if (message.configVersion && configVersion != message.configVersion) {
logger.warn(`Olm ping with outdated config version: ${message.configVersion} (current: ${configVersion})`);
// TODO: sync the client
}
if (olm.userId) { if (olm.userId) {
// we need to check a user token to make sure its still valid // we need to check a user token to make sure its still valid
const { session: userSession, user } = const { session: userSession, user } =
+37 -62
View File
@@ -1,5 +1,4 @@
import { import {
Client,
clientSiteResourcesAssociationsCache, clientSiteResourcesAssociationsCache,
db, db,
orgs, orgs,
@@ -14,7 +13,7 @@ import {
olms, olms,
sites sites
} from "@server/db"; } from "@server/db";
import { and, count, eq, inArray, isNull } from "drizzle-orm"; import { and, eq, inArray, isNull } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers"; import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger"; import logger from "@server/logger";
import { generateAliasConfig } from "@server/lib/ip"; import { generateAliasConfig } from "@server/lib/ip";
@@ -145,64 +144,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.where(eq(clientSitesAssociationsCache.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
} }
// Get all sites data
const sitesCountResult = await db
.select({ count: count() })
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract the count value from the result array
const sitesCount = sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
// Prepare an array to store site configurations
logger.debug(
`Found ${sitesCount} sites for client ${client.clientId}`
);
// this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
logger.warn(
"Client last hole punch is too old and we have sites to send; skipping this register"
);
return;
}
const siteConfigurations = await buildSiteConfigurationForOlmClient(client, publicKey, relay);
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
// if (siteConfigurations.length === 0) {
// logger.warn("No valid site configurations found");
// return;
// }
// Return connect message with all site configurations
return {
message: {
type: "olm/wg/connect",
data: {
sites: siteConfigurations,
tunnelIP: client.subnet,
utilitySubnet: org.utilitySubnet
}
},
broadcast: false,
excludeSender: false
};
};
export async function buildSiteConfigurationForOlmClient(
client: Client,
publicKey: string,
relay: boolean
) {
const siteConfigurations = [];
// Get all sites data // Get all sites data
const sitesData = await db const sitesData = await db
.select() .select()
@@ -213,6 +154,22 @@ export async function buildSiteConfigurationForOlmClient(
) )
.where(eq(clientSitesAssociationsCache.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Prepare an array to store site configurations
const siteConfigurations = [];
logger.debug(
`Found ${sitesData.length} sites for client ${client.clientId}`
);
// this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesData.length > 0) {
logger.warn(
"Client last hole punch is too old and we have sites to send; skipping this register"
);
return;
}
// Process each site // Process each site
for (const { for (const {
sites: site, sites: site,
@@ -332,5 +289,23 @@ export async function buildSiteConfigurationForOlmClient(
}); });
} }
return siteConfigurations; // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
} // if (siteConfigurations.length === 0) {
// logger.warn("No valid site configurations found");
// return;
// }
// Return connect message with all site configurations
return {
message: {
type: "olm/wg/connect",
data: {
sites: siteConfigurations,
tunnelIP: client.subnet,
utilitySubnet: org.utilitySubnet
}
},
broadcast: false,
excludeSender: false
};
};
+1 -3
View File
@@ -5,8 +5,7 @@ import {
handleDockerStatusMessage, handleDockerStatusMessage,
handleDockerContainersMessage, handleDockerContainersMessage,
handleNewtPingRequestMessage, handleNewtPingRequestMessage,
handleApplyBlueprintMessage, handleApplyBlueprintMessage
handleNewtPingMessage
} from "../newt"; } from "../newt";
import { import {
handleOlmRegisterMessage, handleOlmRegisterMessage,
@@ -25,7 +24,6 @@ export const messageHandlers: Record<string, MessageHandler> = {
"olm/wg/relay": handleOlmRelayMessage, "olm/wg/relay": handleOlmRelayMessage,
"olm/wg/unrelay": handleOlmUnRelayMessage, "olm/wg/unrelay": handleOlmUnRelayMessage,
"olm/ping": handleOlmPingMessage, "olm/ping": handleOlmPingMessage,
"newt/ping": handleNewtPingMessage,
"newt/wg/register": handleNewtRegisterMessage, "newt/wg/register": handleNewtRegisterMessage,
"newt/wg/get-config": handleGetConfigMessage, "newt/wg/get-config": handleGetConfigMessage,
"newt/receive-bandwidth": handleReceiveBandwidthMessage, "newt/receive-bandwidth": handleReceiveBandwidthMessage,
+2 -15
View File
@@ -25,7 +25,6 @@ export interface AuthenticatedWebSocket extends WebSocket {
connectionId?: string; connectionId?: string;
isFullyConnected?: boolean; isFullyConnected?: boolean;
pendingMessages?: Buffer[]; pendingMessages?: Buffer[];
configVersion?: number;
} }
export interface TokenPayload { export interface TokenPayload {
@@ -37,7 +36,6 @@ export interface TokenPayload {
export interface WSMessage { export interface WSMessage {
type: string; type: string;
data: any; data: any;
configVersion?: number;
} }
export interface HandlerResponse { export interface HandlerResponse {
@@ -52,15 +50,10 @@ export interface HandlerContext {
senderWs: WebSocket; senderWs: WebSocket;
client: Newt | Olm | RemoteExitNode | undefined; client: Newt | Olm | RemoteExitNode | undefined;
clientType: ClientType; clientType: ClientType;
sendToClient: ( sendToClient: (clientId: string, message: WSMessage) => Promise<boolean>;
clientId: string,
message: WSMessage,
options?: SendMessageOptions
) => Promise<boolean>;
broadcastToAllExcept: ( broadcastToAllExcept: (
message: WSMessage, message: WSMessage,
excludeClientId?: string, excludeClientId?: string
options?: SendMessageOptions
) => Promise<void>; ) => Promise<void>;
connectedClients: Map<string, WebSocket[]>; connectedClients: Map<string, WebSocket[]>;
} }
@@ -69,11 +62,6 @@ export type MessageHandler = (
context: HandlerContext context: HandlerContext
) => Promise<HandlerResponse | void>; ) => Promise<HandlerResponse | void>;
// Options for sending messages with config version tracking
export interface SendMessageOptions {
incrementConfigVersion?: boolean;
}
// Redis message type for cross-node communication // Redis message type for cross-node communication
export interface RedisMessage { export interface RedisMessage {
type: "direct" | "broadcast"; type: "direct" | "broadcast";
@@ -81,5 +69,4 @@ export interface RedisMessage {
excludeClientId?: string; excludeClientId?: string;
message: WSMessage; message: WSMessage;
fromNodeId: string; fromNodeId: string;
options?: SendMessageOptions;
} }
+11 -59
View File
@@ -15,8 +15,7 @@ import {
TokenPayload, TokenPayload,
WebSocketRequest, WebSocketRequest,
WSMessage, WSMessage,
AuthenticatedWebSocket, AuthenticatedWebSocket
SendMessageOptions
} from "./types"; } from "./types";
import { validateSessionToken } from "@server/auth/sessions/app"; import { validateSessionToken } from "@server/auth/sessions/app";
@@ -35,8 +34,6 @@ const NODE_ID = uuidv4();
// Client tracking map (local to this node) // Client tracking map (local to this node)
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map(); const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
// Config version tracking map (clientId -> version)
const clientConfigVersions: Map<string, number> = new Map();
// Helper to get map key // Helper to get map key
const getClientMapKey = (clientId: string) => clientId; const getClientMapKey = (clientId: string) => clientId;
@@ -87,34 +84,14 @@ const removeClient = async (
// Local message sending (within this node) // Local message sending (within this node)
const sendToClientLocal = async ( const sendToClientLocal = async (
clientId: string, clientId: string,
message: WSMessage, message: WSMessage
options: SendMessageOptions = {}
): Promise<boolean> => { ): Promise<boolean> => {
const mapKey = getClientMapKey(clientId); const mapKey = getClientMapKey(clientId);
const clients = connectedClients.get(mapKey); const clients = connectedClients.get(mapKey);
if (!clients || clients.length === 0) { if (!clients || clients.length === 0) {
return false; return false;
} }
const messageString = JSON.stringify(message);
// Increment config version if requested
if (options.incrementConfigVersion) {
const currentVersion = clientConfigVersions.get(clientId) || 0;
const newVersion = currentVersion + 1;
clientConfigVersions.set(clientId, newVersion);
// Update version on all client connections
clients.forEach((client) => {
client.configVersion = newVersion;
});
}
// Include config version in message
const configVersion = clientConfigVersions.get(clientId) || 0;
const messageWithVersion = {
...message,
configVersion
};
const messageString = JSON.stringify(messageWithVersion);
clients.forEach((client) => { clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(messageString); client.send(messageString);
@@ -125,31 +102,14 @@ const sendToClientLocal = async (
const broadcastToAllExceptLocal = async ( const broadcastToAllExceptLocal = async (
message: WSMessage, message: WSMessage,
excludeClientId?: string, excludeClientId?: string
options: SendMessageOptions = {}
): Promise<void> => { ): Promise<void> => {
connectedClients.forEach((clients, mapKey) => { connectedClients.forEach((clients, mapKey) => {
const [type, id] = mapKey.split(":"); const [type, id] = mapKey.split(":");
const clientId = mapKey; // mapKey is the clientId if (!(excludeClientId && id === excludeClientId)) {
if (!(excludeClientId && clientId === excludeClientId)) {
// Handle config version per client
if (options.incrementConfigVersion) {
const currentVersion = clientConfigVersions.get(clientId) || 0;
const newVersion = currentVersion + 1;
clientConfigVersions.set(clientId, newVersion);
clients.forEach((client) => {
client.configVersion = newVersion;
});
}
// Include config version in message for this client
const configVersion = clientConfigVersions.get(clientId) || 0;
const messageWithVersion = {
...message,
configVersion
};
clients.forEach((client) => { clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(messageWithVersion)); client.send(JSON.stringify(message));
} }
}); });
} }
@@ -159,11 +119,10 @@ const broadcastToAllExceptLocal = async (
// Cross-node message sending // Cross-node message sending
const sendToClient = async ( const sendToClient = async (
clientId: string, clientId: string,
message: WSMessage, message: WSMessage
options: SendMessageOptions = {}
): Promise<boolean> => { ): Promise<boolean> => {
// Try to send locally first // Try to send locally first
const localSent = await sendToClientLocal(clientId, message, options); const localSent = await sendToClientLocal(clientId, message);
logger.debug( logger.debug(
`sendToClient: Message type ${message.type} sent to clientId ${clientId}` `sendToClient: Message type ${message.type} sent to clientId ${clientId}`
@@ -174,11 +133,10 @@ const sendToClient = async (
const broadcastToAllExcept = async ( const broadcastToAllExcept = async (
message: WSMessage, message: WSMessage,
excludeClientId?: string, excludeClientId?: string
options: SendMessageOptions = {}
): Promise<void> => { ): Promise<void> => {
// Broadcast locally // Broadcast locally
await broadcastToAllExceptLocal(message, excludeClientId, options); await broadcastToAllExceptLocal(message, excludeClientId);
}; };
// Check if a client has active connections across all nodes // Check if a client has active connections across all nodes
@@ -188,11 +146,6 @@ const hasActiveConnections = async (clientId: string): Promise<boolean> => {
return !!(clients && clients.length > 0); return !!(clients && clients.length > 0);
}; };
// Get the current config version for a client
const getClientConfigVersion = async (clientId: string): Promise<number> => {
return clientConfigVersions.get(clientId) || 0;
};
// Get all active nodes for a client // Get all active nodes for a client
const getActiveNodes = async ( const getActiveNodes = async (
clientType: ClientType, clientType: ClientType,
@@ -481,6 +434,5 @@ export {
getActiveNodes, getActiveNodes,
disconnectClient, disconnectClient,
NODE_ID, NODE_ID,
cleanup, cleanup
getClientConfigVersion
}; };
@@ -285,7 +285,7 @@ export default function Page() {
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
router.push("/admin/idp"); router.push(`/${params.orgId}/settings/idp`);
}} }}
> >
{t("idpSeeAll")} {t("idpSeeAll")}
@@ -1,17 +1,10 @@
import { internal, priv } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable"; import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { cache } from "react";
import {
GetOrgSubscriptionResponse,
GetOrgTierResponse
} from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
type OrgIdpPageProps = { type OrgIdpPageProps = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -35,21 +28,6 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
const t = await getTranslations(); const t = await getTranslations();
let subscriptionStatus: GetOrgTierResponse | null = null;
try {
const getSubscription = cache(() =>
priv.get<AxiosResponse<GetOrgTierResponse>>(
`/org/${params.orgId}/billing/tier`
)
);
const subRes = await getSubscription();
subscriptionStatus = subRes.data.data;
} catch {}
const subscribed =
build === "enterprise"
? true
: subscriptionStatus?.tier === TierId.STANDARD;
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
@@ -57,13 +35,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
description={t("idpManageDescription")} description={t("idpManageDescription")}
/> />
{build === "saas" && !subscribed ? ( <PaidFeaturesAlert />
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("idpDisabled")} {t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<IdpTable idps={idps} orgId={params.orgId} /> <IdpTable idps={idps} orgId={params.orgId} />
</> </>
@@ -164,6 +164,10 @@ function MaintenanceSectionForm({
return isEnterpriseNotLicensed || isSaasNotSubscribed; return isEnterpriseNotLicensed || isSaasNotSubscribed;
}; };
if (!resource.http) {
return null;
}
return ( return (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
@@ -437,9 +441,16 @@ export default function GeneralForm() {
); );
const resourceFullDomainName = useMemo(() => { const resourceFullDomainName = useMemo(() => {
const url = new URL(resourceFullDomain); if (!resource.fullDomain) {
return url.hostname; return "";
}, [resourceFullDomain]); }
try {
const url = new URL(resourceFullDomain);
return url.hostname;
} catch {
return "";
}
}, [resourceFullDomain, resource.fullDomain]);
const [selectedDomain, setSelectedDomain] = useState<{ const [selectedDomain, setSelectedDomain] = useState<{
domainId: string; domainId: string;
@@ -338,7 +338,7 @@ function ProxyResourceTargetsForm({
<div <div
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`} className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
> >
<Settings className="h-3 w-3" /> <Settings className="h-4 w-4 text-foreground" />
{getStatusText(status)} {getStatusText(status)}
</div> </div>
</Button> </Button>
+12
View File
@@ -178,4 +178,16 @@ p {
.animate-dot-pulse { .animate-dot-pulse {
animation: dot-pulse 1.4s ease-in-out infinite; animation: dot-pulse 1.4s ease-in-out infinite;
} }
/* Use JavaScript-set viewport height for mobile to handle keyboard properly */
.h-screen-safe {
height: 100vh; /* Default for desktop and fallback */
}
/* Only apply custom viewport height on mobile */
@media (max-width: 767px) {
.h-screen-safe {
height: var(--vh, 100vh); /* Use CSS variable set by ViewportHeightFix on mobile */
}
}
} }
+3 -1
View File
@@ -22,6 +22,7 @@ import { TopLoader } from "@app/components/Toploader";
import Script from "next/script"; import Script from "next/script";
import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider"; import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
import { TailwindIndicator } from "@app/components/TailwindIndicator"; import { TailwindIndicator } from "@app/components/TailwindIndicator";
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -77,7 +78,7 @@ export default async function RootLayout({
return ( return (
<html suppressHydrationWarning lang={locale}> <html suppressHydrationWarning lang={locale}>
<body className={`${font.className} h-screen overflow-hidden`}> <body className={`${font.className} h-screen-safe overflow-hidden`}>
<TopLoader /> <TopLoader />
{build === "saas" && ( {build === "saas" && (
<Script <Script
@@ -86,6 +87,7 @@ export default async function RootLayout({
strategy="afterInteractive" strategy="afterInteractive"
/> />
)} )}
<ViewportHeightFix />
<NextIntlClientProvider> <NextIntlClientProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
+2 -2
View File
@@ -37,7 +37,7 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed); (sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
return ( return (
<div className="flex h-screen overflow-hidden"> <div className="flex h-screen-safe overflow-hidden">
{/* Desktop Sidebar */} {/* Desktop Sidebar */}
{showSidebar && ( {showSidebar && (
<LayoutSidebar <LayoutSidebar
@@ -75,7 +75,7 @@ export async function Layout({
<div <div
className={cn( className={cn(
"container mx-auto max-w-12xl mb-12", "container mx-auto max-w-12xl mb-12",
showHeader && "pt-16 md:pt-16" // Add top padding on mobile and desktop to account for fixed header showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
)} )}
> >
{children} {children}
+1 -1
View File
@@ -48,7 +48,7 @@ export function LayoutMobileMenu({
const t = useTranslations(); const t = useTranslations();
return ( return (
<div className="shrink-0 md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b border-border"> <div className="shrink-0 md:hidden sticky top-0 z-50">
<div className="h-16 flex items-center px-2"> <div className="h-16 flex items-center px-2">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{showSidebar && ( {showSidebar && (
+1 -1
View File
@@ -198,7 +198,7 @@ export default function ProxyResourcesTable({
if (!targets || targets.length === 0) { if (!targets || targets.length === 0) {
return ( return (
<div className="flex items-center gap-2"> <div id="LOOK_FOR_ME" className="flex items-center gap-2">
<StatusIcon status="unknown" /> <StatusIcon status="unknown" />
<span className="text-sm"> <span className="text-sm">
{t("resourcesTableNoTargets")} {t("resourcesTableNoTargets")}
+6 -6
View File
@@ -32,12 +32,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSections <InfoSections
cols={resource.http && env.flags.usePangolinDns ? 5 : 4} cols={resource.http && env.flags.usePangolinDns ? 5 : 4}
> >
<InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={fullUrl} isLink={true} />
</InfoSectionContent>
</InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle> <InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
@@ -46,6 +40,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSection> </InfoSection>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={fullUrl} isLink={true} />
</InfoSectionContent>
</InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("authentication")} {t("authentication")}
+79
View File
@@ -0,0 +1,79 @@
"use client";
import { useEffect } from "react";
/**
* Fixes mobile viewport height issues when keyboard opens/closes
* by setting a CSS variable with a stable viewport height
* Only applies on mobile devices (< 768px, matching Tailwind's md breakpoint)
*/
export function ViewportHeightFix() {
useEffect(() => {
// Check if we're on mobile (md breakpoint is typically 768px)
const isMobile = () => window.innerWidth < 768;
// On desktop, don't set --vh at all, let CSS use 100vh directly
if (!isMobile()) {
// Remove --vh if it was set, so CSS falls back to 100vh
document.documentElement.style.removeProperty("--vh");
return;
}
// Mobile-specific logic
let maxHeight = window.innerHeight;
let resizeTimer: NodeJS.Timeout;
// Set the viewport height as a CSS variable
const setViewportHeight = (height: number) => {
document.documentElement.style.setProperty("--vh", `${height}px`);
};
// Set initial value
setViewportHeight(maxHeight);
const handleResize = () => {
// If we switched to desktop, remove --vh and stop
if (!isMobile()) {
document.documentElement.style.removeProperty("--vh");
return;
}
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const currentHeight = window.innerHeight;
// Track the maximum height we've seen (when keyboard is closed)
if (currentHeight > maxHeight) {
maxHeight = currentHeight;
setViewportHeight(maxHeight);
}
// If current height is close to max, update max (keyboard closed)
else if (currentHeight >= maxHeight * 0.9) {
maxHeight = currentHeight;
setViewportHeight(maxHeight);
}
// Otherwise, keep using the max height (keyboard is open)
}, 100);
};
const handleOrientationChange = () => {
// Reset on orientation change
setTimeout(() => {
maxHeight = window.innerHeight;
setViewportHeight(maxHeight);
}, 150);
};
window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleOrientationChange);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("orientationchange", handleOrientationChange);
clearTimeout(resizeTimer);
};
}, []);
return null;
}
+19 -24
View File
@@ -73,35 +73,30 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
> >
{asChild ? ( {asChild ? (
props.children props.children
) : ( ) : loading ? (
<span className="relative inline-flex items-center justify-center"> <span className="relative inline-flex items-center justify-center">
<span <span className="inline-flex items-center justify-center opacity-0">
className={cn(
"inline-flex items-center justify-center",
loading && "opacity-0"
)}
>
{props.children} {props.children}
</span> </span>
{loading && ( <span className="absolute inset-0 flex items-center justify-center">
<span className="absolute inset-0 flex items-center justify-center"> <span className="flex items-center gap-1.5">
<span className="flex items-center gap-1"> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "0ms" }}
style={{ animationDelay: "0ms" }} />
/> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "200ms" }}
style={{ animationDelay: "200ms" }} />
/> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "400ms" }}
style={{ animationDelay: "400ms" }} />
/>
</span>
</span> </span>
)} </span>
</span> </span>
) : (
props.children
)} )}
</Comp> </Comp>
); );
+7 -9
View File
@@ -14,13 +14,13 @@ const checkboxVariants = cva(
variants: { variants: {
variant: { variant: {
outlinePrimary: outlinePrimary:
"border rounded-[5px] border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
outline: outline:
"border rounded-[5px] border-input data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground",
outlinePrimarySquare: outlinePrimarySquare:
"border rounded-[5px] border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
outlineSquare: outlineSquare:
"border rounded-[5px] border-input data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground" "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground"
} }
}, },
defaultVariants: { defaultVariants: {
@@ -30,8 +30,7 @@ const checkboxVariants = cva(
); );
interface CheckboxProps interface CheckboxProps
extends extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
VariantProps<typeof checkboxVariants> {} VariantProps<typeof checkboxVariants> {}
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
@@ -50,9 +49,8 @@ const Checkbox = React.forwardRef<
)); ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName; Checkbox.displayName = CheckboxPrimitive.Root.displayName;
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef< interface CheckboxWithLabelProps
typeof Checkbox extends React.ComponentPropsWithoutRef<typeof Checkbox> {
> {
label: string; label: string;
} }