diff --git a/messages/bg-BG.json b/messages/bg-BG.json index d429f4bd6..2d6fead50 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -898,6 +898,7 @@ "idpDisplayName": "Име за показване за този доставчик на идентичност", "idpAutoProvisionUsers": "Автоматично потребителско създаване", "idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.", + "idpAutoProvisionConfigureAfterCreate": "Можете да конфигурирате настройките за автоматично предоставяне, след като дистрибуторът на самоличност бъде създаден.", "licenseBadge": "ЕЕ", "idpType": "Тип доставчик", "idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Карта на роля по подразбиране", "defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.", "defaultMappingsOrg": "Карта на организация по подразбиране", - "defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.", + "defaultMappingsOrgDescription": "При задаване, този израз трябва да върне идентификационния номер на организацията или true, за да се даде достъп на потребителя до тази организация. Ако не е зададено, дефинирането на роля е достатъчно: потребителят има право на достъп, стига валидно картографиране на роля да бъде разрешено за него в рамките на организацията.", "defaultMappingsSubmit": "Запазване на файловете по подразбиране", "orgPoliciesEdit": "Редактиране на Организационна Политика", "org": "Организация", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Открит международен домейн", "willbestoredas": "Ще бъде съхранено като:", - "roleMappingDescription": "Определете как се разпределят ролите на потребителите при вписване, когато е активирано автоматично предоставяне.", + "roleMappingDescription": "Определете как ролите се присвояват на потребителите, когато се вписват с този доставчик на самоличност.", "selectRole": "Избор на роля", "roleMappingExpression": "Израз", "selectRolePlaceholder": "Избор на роля", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно", "httpDestCreatedSuccess": "Дестинацията беше създадена успешно", "httpDestUpdateFailed": "Неуспешно актуализиране на дестинацията", - "httpDestCreateFailed": "Неуспешно създаване на дестинацията" + "httpDestCreateFailed": "Неуспешно създаване на дестинацията", + "idpAddActionCreateNew": "Създайте нов доставчик на самоличност", + "idpAddActionImportFromOrg": "Импортиране от друга организация", + "idpImportDialogTitle": "Импортиране на доставчик на самоличност", + "idpImportDialogDescription": "Изберете доставчик на самоличност от организация, в която сте администратор. Той ще бъде свързан с тази организация.", + "idpImportSearchPlaceholder": "Търсене по име на организация или доставчик...", + "idpImportEmpty": "Няма намерени доставчици на самоличност.", + "idpImportedDescription": "Доставчикът на самоличност беше импортиран успешно.", + "idpDeleteGlobalQuestion": "Сигурни ли сте, че искате да изтриете този доставчик на самоличност завинаги?", + "idpDeleteGlobalDescription": "Това ще изтрие доставичка на самоличност завинаги от всички организации, с които е свързан.", + "idpUnassociateTitle": "Отвързване на доставчик на самоличност", + "idpUnassociateQuestion": "Сигурни ли сте, че искате да отвържете този доставчик на самоличност от тази организация?", + "idpUnassociateDescription": "Всички потребители, свързани с този доставчик на самоличност, ще бъдат премахнати от тази организация, но доставчика на самоличност ще продължи да съществува за други свързани организации.", + "idpUnassociateConfirm": "Потвърдете отвързване на доставчика на самоличност", + "idpUnassociateWarning": "Това не може да бъде отменено за тази организация.", + "idpUnassociatedDescription": "Доставчика на самоличност е успешно отвързан от тази организация", + "idpUnassociateMenu": "Отвързване", + "idpDeleteAllOrgsMenu": "Изтриване" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 66cee2a8b..e6e952e4b 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -898,6 +898,7 @@ "idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity", "idpAutoProvisionUsers": "Automatická úprava uživatelů", "idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.", + "idpAutoProvisionConfigureAfterCreate": "Nastavení automatického poskytování lze nakonfigurovat, jakmile je vytvořen poskytovatel identity.", "licenseBadge": "PE", "idpType": "Typ poskytovatele", "idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Výchozí mapování rolí", "defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.", "defaultMappingsOrg": "Výchozí mapování organizace", - "defaultMappingsOrgDescription": "Tento výraz musí vrátit org ID nebo pravdu, aby měl uživatel přístup k organizaci.", + "defaultMappingsOrgDescription": "Pokud je nastaven, musí tento výraz vracet ID organizace nebo pravda, aby k této organizaci měl uživatel přístup. Pokud není nastaveno, je dostačující definice mapování rolí: uživateli je umožněn přístup, pokud pro něj lze v rámci organizace vyřešit platné mapování rolí.", "defaultMappingsSubmit": "Uložit výchozí mapování", "orgPoliciesEdit": "Upravit zásady organizace", "org": "Organizace", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Zjištěna mezinárodní doména", "willbestoredas": "Bude uloženo jako:", - "roleMappingDescription": "Určete, jak jsou role přiřazeny uživatelům, když se přihlásí, když je povoleno automatické poskytnutí služby.", + "roleMappingDescription": "Určete, jak jsou role přiřazeny uživatelům, když se přihlásí s tímto poskytovatelem identity.", "selectRole": "Vyberte roli", "roleMappingExpression": "Výraz", "selectRolePlaceholder": "Vyberte roli", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Cíl byl úspěšně aktualizován", "httpDestCreatedSuccess": "Cíl byl úspěšně vytvořen", "httpDestUpdateFailed": "Nepodařilo se aktualizovat cíl", - "httpDestCreateFailed": "Nepodařilo se vytvořit cíl" + "httpDestCreateFailed": "Nepodařilo se vytvořit cíl", + "idpAddActionCreateNew": "Vytvořit nového poskytovatele identity", + "idpAddActionImportFromOrg": "Importovat z jiné organizace", + "idpImportDialogTitle": "Importovat poskytovatele identity", + "idpImportDialogDescription": "Vyberte poskytovatele identity z organizace, v níž jste administrátor. Tento poskytovatel bude propojen s touto organizací.", + "idpImportSearchPlaceholder": "Hledat podle názvu organizace nebo poskytovatele...", + "idpImportEmpty": "Nebyli nalezeni žádní poskytovatelé identity.", + "idpImportedDescription": "Poskytovatel identity byl úspěšně importován.", + "idpDeleteGlobalQuestion": "Opravdu chcete trvale smazat tohoto poskytovatele identity?", + "idpDeleteGlobalDescription": "Tímto bude poskytovatel identity trvale odstraněn ze všech organizací, se kterými je spojen.", + "idpUnassociateTitle": "Odpojit poskytovatele identity", + "idpUnassociateQuestion": "Opravdu chcete odpojit tohoto poskytovatele identity od této organizace?", + "idpUnassociateDescription": "Všichni uživatelé spojení s tímto poskytovatelem identity budou odstraněni z této organizace, ale poskytovatel identity zůstane nadále existovat pro ostatní přidružené organizace.", + "idpUnassociateConfirm": "Potvrdit odpojení poskytovatele identity", + "idpUnassociateWarning": "Toto nelze pro tuto organizaci vrátit.", + "idpUnassociatedDescription": "Poskytovatel identity byl úspěšně odpojen od této organizace", + "idpUnassociateMenu": "Odpojit", + "idpDeleteAllOrgsMenu": "Odstranit" } diff --git a/messages/de-DE.json b/messages/de-DE.json index 4ea6c9fe6..43e055c3b 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -898,6 +898,7 @@ "idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter", "idpAutoProvisionUsers": "Automatische Benutzerbereitstellung", "idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.", + "idpAutoProvisionConfigureAfterCreate": "Sie können die automatische Bereitstellung einstellen, sobald der Identitätsanbieter erstellt ist.", "licenseBadge": "EE", "idpType": "Anbietertyp", "idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Standard-Rollenzuordnung", "defaultMappingsRoleDescription": "JMESPath zur Extraktion von Rolleninformationen aus dem ID-Token. Das Ergebnis dieses Ausdrucks muss den Rollennamen als String zurückgeben, wie er in der Organisation definiert ist.", "defaultMappingsOrg": "Standard-Organisationszuordnung", - "defaultMappingsOrgDescription": "JMESPath zur Extraktion von Organisationsinformationen aus dem ID-Token. Dieser Ausdruck muss die Organisations-ID oder true zurückgeben, damit der Benutzer Zugriff auf die Organisation erhält.", + "defaultMappingsOrgDescription": "Wenn diese Einstellung festgelegt ist, muss dieser Ausdruck die Organisations-ID oder wahr zurückgeben, damit der Benutzer diese Organisation betreten kann. Ist sie nicht festgelegt, reicht die Definition einer Rollenzuordnung aus: Der Benutzer darf eintreten, solange eine gültige Rollenzuordnung innerhalb der Organisation für ihn aufgelöst werden kann.", "defaultMappingsSubmit": "Standardzuordnungen speichern", "orgPoliciesEdit": "Organisationsrichtlinie bearbeiten", "org": "Organisation", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Internationale Domain erkannt", "willbestoredas": "Wird gespeichert als:", - "roleMappingDescription": "Legen Sie fest, wie den Benutzern Rollen zugewiesen werden, wenn sie sich anmelden, wenn Auto Provision aktiviert ist.", + "roleMappingDescription": "Bestimmen Sie, wie Rollen zugewiesen werden, wenn sich Benutzer mit diesem Identitätsanbieter anmelden.", "selectRole": "Wählen Sie eine Rolle", "roleMappingExpression": "Ausdruck", "selectRolePlaceholder": "Rolle auswählen", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert", "httpDestCreatedSuccess": "Ziel erfolgreich erstellt", "httpDestUpdateFailed": "Fehler beim Aktualisieren des Ziels", - "httpDestCreateFailed": "Fehler beim Erstellen des Ziels" + "httpDestCreateFailed": "Fehler beim Erstellen des Ziels", + "idpAddActionCreateNew": "Neuen Identitätsanbieter erstellen", + "idpAddActionImportFromOrg": "Von einer anderen Organisation importieren", + "idpImportDialogTitle": "Identitätsanbieter importieren", + "idpImportDialogDescription": "Wählen Sie einen Identitätsanbieter aus einer Organisation, in der Sie Administrator sind. Er wird mit dieser Organisation verknüpft.", + "idpImportSearchPlaceholder": "Nach Organisation oder Anbieternamen suchen...", + "idpImportEmpty": "Keine Identitätsanbieter gefunden.", + "idpImportedDescription": "Identitätsanbieter erfolgreich importiert.", + "idpDeleteGlobalQuestion": "Sind Sie sicher, dass Sie diesen Identitätsanbieter dauerhaft löschen möchten?", + "idpDeleteGlobalDescription": "Dies wird den Identitätsanbieter dauerhaft von allen Organisationen löschen, mit denen er verbunden ist.", + "idpUnassociateTitle": "Verknüpfung mit Identitätsanbieter aufheben", + "idpUnassociateQuestion": "Sind Sie sicher, dass Sie die Verknüpfung dieses Identitätsanbieters mit dieser Organisation aufheben möchten?", + "idpUnassociateDescription": "Alle Benutzer, die mit diesem Identitätsanbieter verbunden sind, werden aus dieser Organisation entfernt, aber der Identitätsanbieter bleibt für andere verbundene Organisationen weiterhin bestehen.", + "idpUnassociateConfirm": "Verknüpfung des Identitätsanbieters aufheben bestätigen", + "idpUnassociateWarning": "Dies kann für diese Organisation nicht rückgängig gemacht werden.", + "idpUnassociatedDescription": "Identitätsanbieter erfolgreich von dieser Organisation gelöst", + "idpUnassociateMenu": "Verknüpfung aufheben", + "idpDeleteAllOrgsMenu": "Löschen" } diff --git a/messages/en-US.json b/messages/en-US.json index 5bb1af511..856586907 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -902,6 +902,7 @@ "idpDisplayName": "A display name for this identity provider", "idpAutoProvisionUsers": "Auto Provision Users", "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", + "idpAutoProvisionConfigureAfterCreate": "You can configure auto provision settings once the identity provider is created.", "licenseBadge": "EE", "idpType": "Provider Type", "idpTypeDescription": "Select the type of identity provider you want to configure", @@ -953,7 +954,7 @@ "defaultMappingsRole": "Default Role Mapping", "defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.", "defaultMappingsOrg": "Default Organization Mapping", - "defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.", + "defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining a role mapping is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.", "defaultMappingsSubmit": "Save Default Mappings", "orgPoliciesEdit": "Edit Organization Policy", "org": "Organization", @@ -2152,7 +2153,7 @@ }, "internationaldomaindetected": "International Domain Detected", "willbestoredas": "Will be stored as:", - "roleMappingDescription": "Determine how roles are assigned to users when they sign in when Auto Provision is enabled.", + "roleMappingDescription": "Determine how roles are assigned to users when they sign in with this identity provider.", "selectRole": "Select a Role", "roleMappingExpression": "Expression", "selectRolePlaceholder": "Choose a role", @@ -3052,5 +3053,23 @@ "healthCheckTabConnection": "Connection", "healthCheckTabAdvanced": "Advanced", "healthCheckStrategyNotAvailable": "This strategy is not available. Please contact sales to enable this feature.", - "uptime30d": "Uptime (30d)" + "uptime30d": "Uptime (30d)", + "idpAddActionCreateNew": "Create new identity provider", + "idpAddActionImportFromOrg": "Import from another organization", + "idpImportDialogTitle": "Import Identity Provider", + "idpImportDialogDescription": "Choose an identity provider from an organization where you are an admin. It will be linked to this organization.", + "idpImportSearchPlaceholder": "Search by organization or provider name...", + "idpImportEmpty": "No identity providers found.", + "idpImportedDescription": "Identity provider imported successfully.", + "idpDeleteGlobalQuestion": "Are you sure you want to permanently delete this identity provider?", + "idpDeleteGlobalDescription": "This will permanently delete the identity provider from all organizations it is associated with.", + "idpUnassociateTitle": "Unassociate Identity Provider", + "idpUnassociateQuestion": "Are you sure you want to unassociate this identity provider from this organization?", + "idpUnassociateDescription": "All users associated with this identity provider will be removed from this organization, but the identity provider will still continue to exist for other associated organizations.", + "idpUnassociateConfirm": "Confirm Unassociate Identity Provider", + "idpUnassociateWarning": "This cannot be undone for this organization.", + "idpUnassociatedDescription": "Identity provider unassociated from this organization successfully", + "idpUnassociateMenu": "Unassociate", + "idpDeleteAllOrgsMenu": "Delete", + "publicIpEndpoint": "Endpoint" } diff --git a/messages/es-ES.json b/messages/es-ES.json index 0fa9201c8..b370ee7dc 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -898,6 +898,7 @@ "idpDisplayName": "Un nombre mostrado para este proveedor de identidad", "idpAutoProvisionUsers": "Auto-Provisión de Usuarios", "idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.", + "idpAutoProvisionConfigureAfterCreate": "Puede configurar las configuraciones de provisión automática una vez que se haya creado el proveedor de identidad.", "licenseBadge": "EE", "idpType": "Tipo de proveedor", "idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Mapeo de Rol por defecto", "defaultMappingsRoleDescription": "El resultado de esta expresión debe devolver el nombre del rol tal y como se define en la organización como una cadena.", "defaultMappingsOrg": "Mapeo de organización por defecto", - "defaultMappingsOrgDescription": "Esta expresión debe devolver el ID de org o verdadero para que el usuario pueda acceder a la organización.", + "defaultMappingsOrgDescription": "Cuando se establece, esta expresión debe devolver el ID de la organización o verdadero para que el usuario acceda a esa organización. Cuando no se establece, definir un mapeo de roles es suficiente: se permite la entrada del usuario siempre que se pueda resolver un mapeo de roles válido para él dentro de la organización.", "defaultMappingsSubmit": "Guardar asignaciones por defecto", "orgPoliciesEdit": "Editar Política de Organización", "org": "Organización", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Dominio Internacional detectado", "willbestoredas": "Se almacenará como:", - "roleMappingDescription": "Determinar cómo se asignan los roles a los usuarios cuando se registran cuando está habilitada la provisión automática.", + "roleMappingDescription": "Determine cómo se asignan los roles a los usuarios cuando inician sesión con este proveedor de identidad.", "selectRole": "Seleccione un rol", "roleMappingExpression": "Expresión", "selectRolePlaceholder": "Elija un rol", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Destino actualizado correctamente", "httpDestCreatedSuccess": "Destino creado correctamente", "httpDestUpdateFailed": "Error al actualizar destino", - "httpDestCreateFailed": "Error al crear el destino" + "httpDestCreateFailed": "Error al crear el destino", + "idpAddActionCreateNew": "Crear nuevo proveedor de identidad", + "idpAddActionImportFromOrg": "Importar de otra organización", + "idpImportDialogTitle": "Importar Proveedor de Identidad", + "idpImportDialogDescription": "Elija un proveedor de identidad de una organización donde usted sea administrador. Se vinculará a esta organización.", + "idpImportSearchPlaceholder": "Buscar por nombre de organización o proveedor...", + "idpImportEmpty": "No se encontraron proveedores de identidad.", + "idpImportedDescription": "Proveedor de identidad importado con éxito.", + "idpDeleteGlobalQuestion": "¿Está seguro de que desea eliminar permanentemente este proveedor de identidad?", + "idpDeleteGlobalDescription": "Esto eliminará permanentemente el proveedor de identidad de todas las organizaciones con las que está asociado.", + "idpUnassociateTitle": "Desasociar Proveedor de Identidad", + "idpUnassociateQuestion": "¿Está seguro de que desea desasociar este proveedor de identidad de esta organización?", + "idpUnassociateDescription": "Todos los usuarios asociados con este proveedor de identidad serán eliminados de esta organización, pero el proveedor de identidad continuará existiendo para otras organizaciones asociadas.", + "idpUnassociateConfirm": "Confirme Desasociar Proveedor de Identidad", + "idpUnassociateWarning": "Esto no se puede deshacer para esta organización.", + "idpUnassociatedDescription": "Proveedor de identidad desasociado de esta organización con éxito", + "idpUnassociateMenu": "Desasociar", + "idpDeleteAllOrgsMenu": "Eliminar" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 419701b5f..98b769366 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -898,6 +898,7 @@ "idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité", "idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs", "idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.", + "idpAutoProvisionConfigureAfterCreate": "Vous pouvez configurer les paramètres de provisionnement automatique une fois le fournisseur d'identités créé.", "licenseBadge": "EE", "idpType": "Type de fournisseur", "idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Mappage de rôle par défaut", "defaultMappingsRoleDescription": "JMESPath pour extraire les informations de rôle du jeton ID. Le résultat de cette expression doit renvoyer le nom du rôle tel que défini dans l'organisation sous forme de chaîne.", "defaultMappingsOrg": "Mappage d'organisation par défaut", - "defaultMappingsOrgDescription": "JMESPath pour extraire les informations d'organisation du jeton ID. Cette expression doit renvoyer l'ID de l'organisation ou true pour que l'utilisateur soit autorisé à accéder à l'organisation.", + "defaultMappingsOrgDescription": "Lorsque défini, cette expression doit renvoyer l'identifiant de l'organisation ou vrai pour que l'utilisateur accède à cette organisation. Lorsqu'indéfini, définir un mappage de rôle est suffisant : l'utilisateur est autorisé tant qu'un mappage de rôle valide peut être résolu pour lui au sein de l'organisation.", "defaultMappingsSubmit": "Enregistrer les mappages par défaut", "orgPoliciesEdit": "Modifier la politique d'organisation", "org": "Organisation", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Domaine international détecté", "willbestoredas": "Sera stocké comme :", - "roleMappingDescription": "Détermine comment les rôles sont assignés aux utilisateurs lorsqu'ils se connectent lorsque la fourniture automatique est activée.", + "roleMappingDescription": "Déterminez comment les rôles sont attribués aux utilisateurs lorsqu'ils se connectent avec ce fournisseur d'identité.", "selectRole": "Sélectionnez un rôle", "roleMappingExpression": "Expression", "selectRolePlaceholder": "Choisir un rôle", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Destination mise à jour avec succès", "httpDestCreatedSuccess": "Destination créée avec succès", "httpDestUpdateFailed": "Impossible de mettre à jour la destination", - "httpDestCreateFailed": "Impossible de créer la destination" + "httpDestCreateFailed": "Impossible de créer la destination", + "idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité", + "idpAddActionImportFromOrg": "Importer d'une autre organisation", + "idpImportDialogTitle": "Importer le fournisseur d'identité", + "idpImportDialogDescription": "Choisissez un fournisseur d'identités d'une organisation où vous êtes administrateur. Il sera lié à cette organisation.", + "idpImportSearchPlaceholder": "Recherche par nom d'organisation ou de fournisseur...", + "idpImportEmpty": "Aucun fournisseur d'identités trouvé.", + "idpImportedDescription": "Fournisseur d'identités importé avec succès.", + "idpDeleteGlobalQuestion": "Êtes-vous sûr de vouloir supprimer définitivement ce fournisseur d'identités?", + "idpDeleteGlobalDescription": "Cela supprimera définitivement le fournisseur d'identités de toutes les organisations auxquelles il est associé.", + "idpUnassociateTitle": "Dissocier le fournisseur d'identité", + "idpUnassociateQuestion": "Êtes-vous sûr de vouloir dissocier ce fournisseur d'identités de cette organisation?", + "idpUnassociateDescription": "Tous les utilisateurs associés à ce fournisseur d'identités seront retirés de cette organisation, mais le fournisseur d'identités continuera d'exister pour d'autres organisations associées.", + "idpUnassociateConfirm": "Confirmer la dissociation du fournisseur d'identités", + "idpUnassociateWarning": "Cela ne peut pas être annulé pour cette organisation.", + "idpUnassociatedDescription": "Fournisseur d'identités dissocié de cette organisation avec succès", + "idpUnassociateMenu": "Dissocier", + "idpDeleteAllOrgsMenu": "Supprimer" } diff --git a/messages/it-IT.json b/messages/it-IT.json index e761ea55f..babe33b59 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -898,6 +898,7 @@ "idpDisplayName": "Un nome visualizzato per questo provider di identità", "idpAutoProvisionUsers": "Provisioning Automatico Utenti", "idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.", + "idpAutoProvisionConfigureAfterCreate": "Puoi configurare le impostazioni di auto fornitura una volta creato il provider di identità.", "licenseBadge": "EE", "idpType": "Tipo di Provider", "idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Mappatura Ruolo Predefinito", "defaultMappingsRoleDescription": "JMESPath per estrarre informazioni sul ruolo dal token ID. Il risultato di questa espressione deve restituire il nome del ruolo come definito nell'organizzazione come stringa.", "defaultMappingsOrg": "Mappatura Organizzazione Predefinita", - "defaultMappingsOrgDescription": "JMESPath per estrarre informazioni sull'organizzazione dal token ID. Questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere all'organizzazione.", + "defaultMappingsOrgDescription": "Quando impostata, questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere a quell'organizzazione. Quando non impostata, è sufficiente definire una mappatura di ruoli: l'utente è autorizzato se esiste una mappatura di ruolo valida per loro all'interno dell'organizzazione.", "defaultMappingsSubmit": "Salva Mappature Predefinite", "orgPoliciesEdit": "Modifica Politica Organizzazione", "org": "Organizzazione", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Dominio Internazionale Rilevato", "willbestoredas": "Verrà conservato come:", - "roleMappingDescription": "Determinare come i ruoli sono assegnati agli utenti quando accedono quando è abilitata la fornitura automatica.", + "roleMappingDescription": "Determina come i ruoli vengono assegnati agli utenti quando si accede con questo provider di identità.", "selectRole": "Seleziona un ruolo", "roleMappingExpression": "Espressione", "selectRolePlaceholder": "Scegli un ruolo", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Destinazione aggiornata con successo", "httpDestCreatedSuccess": "Destinazione creata con successo", "httpDestUpdateFailed": "Impossibile aggiornare la destinazione", - "httpDestCreateFailed": "Impossibile creare la destinazione" + "httpDestCreateFailed": "Impossibile creare la destinazione", + "idpAddActionCreateNew": "Crea nuovo provider di identità", + "idpAddActionImportFromOrg": "Importa da un'altra organizzazione", + "idpImportDialogTitle": "Importa Provider di Identità", + "idpImportDialogDescription": "Scegli un provider di identità da un'organizzazione di cui sei amministratore. Verrà collegato a questa organizzazione.", + "idpImportSearchPlaceholder": "Cerca per nome organizzazione o provider...", + "idpImportEmpty": "Nessun provider di identità trovato.", + "idpImportedDescription": "Provider di identità importato con successo.", + "idpDeleteGlobalQuestion": "Sei sicuro di voler eliminare definitivamente questo provider di identità?", + "idpDeleteGlobalDescription": "Questo eliminerà definitivamente il provider di identità da tutte le organizzazioni con cui è associato.", + "idpUnassociateTitle": "Disassociare Provider di Identità", + "idpUnassociateQuestion": "Sei sicuro di voler disassociare questo provider di identità da questa organizzazione?", + "idpUnassociateDescription": "Tutti gli utenti associati a questo provider di identità verranno rimossi da questa organizzazione, ma il provider di identità continuerà ad esistere per altre organizzazioni associate.", + "idpUnassociateConfirm": "Conferma Disassociazione Provider di Identità", + "idpUnassociateWarning": "Questo non può essere annullato per questa organizzazione.", + "idpUnassociatedDescription": "Provider di identità disassociato con successo da questa organizzazione", + "idpUnassociateMenu": "Disassocia", + "idpDeleteAllOrgsMenu": "Elimina" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index f394fa2d6..9e55b0d32 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -898,6 +898,7 @@ "idpDisplayName": "이 신원 공급자를 위한 표시 이름", "idpAutoProvisionUsers": "사용자 자동 프로비저닝", "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", + "idpAutoProvisionConfigureAfterCreate": "아이덴티티 공급자가 생성되면 자동 프로비저닝 설정을 구성할 수 있습니다.", "licenseBadge": "EE", "idpType": "제공자 유형", "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", @@ -949,7 +950,7 @@ "defaultMappingsRole": "기본 역할 매핑", "defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.", "defaultMappingsOrg": "기본 조직 매핑", - "defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다.", + "defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다. 설정되지 않으면, 역할 매핑 정의가 충분합니다: 사용자는 유효한 역할 매핑이 해석되는 한 조직에 허용됩니다.", "defaultMappingsSubmit": "기본 매핑 저장", "orgPoliciesEdit": "조직 정책 편집", "org": "조직", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "국제 도메인 감지됨", "willbestoredas": "다음으로 저장됩니다:", - "roleMappingDescription": "자동 프로비저닝이 활성화되면 사용자가 로그인할 때 역할이 할당되는 방법을 결정합니다.", + "roleMappingDescription": "사용자가 이 아이덴티티 공급자로 로그인할 때 역할이 할당되는 방법을 결정합니다.", "selectRole": "역할 선택", "roleMappingExpression": "표현식", "selectRolePlaceholder": "역할 선택", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다", "httpDestCreatedSuccess": "대상지가 성공적으로 생성되었습니다", "httpDestUpdateFailed": "대상지를 업데이트하는 데 실패했습니다", - "httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다" + "httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다", + "idpAddActionCreateNew": "새로운 아이덴티티 공급자 생성", + "idpAddActionImportFromOrg": "다른 조직에서 가져오기", + "idpImportDialogTitle": "아이덴티티 공급자 가져오기", + "idpImportDialogDescription": "관리자인 조직에서 아이덴티티 공급자를 선택하십시오. 이는 이 조직에 연결됩니다.", + "idpImportSearchPlaceholder": "조직 또는 공급자 이름으로 검색...", + "idpImportEmpty": "아이덴티티 공급자를 찾을 수 없습니다.", + "idpImportedDescription": "아이덴티티 공급자가 성공적으로 가져왔습니다.", + "idpDeleteGlobalQuestion": "정말로 이 아이덴티티 공급자를 영구적으로 삭제하시겠습니까?", + "idpDeleteGlobalDescription": "이것은 연관된 모든 조직에서 아이덴티티 공급자를 영구적으로 삭제합니다.", + "idpUnassociateTitle": "아이덴티티 공급자의 연관 해제", + "idpUnassociateQuestion": "정말로 이 조직에서 이 아이덴티티 공급자의 연관을 해제하시겠습니까?", + "idpUnassociateDescription": "이 아이덴티티 공급자와 연관된 모든 사용자는 이 조직에서 제거될 것이지만, 아이덴티티 공급자는 다른 연관된 조직에 계속해서 존재할 것입니다.", + "idpUnassociateConfirm": "아이덴티티 공급자 연관 해제 확인", + "idpUnassociateWarning": "이 조직에서 이것은 되돌릴 수 없습니다.", + "idpUnassociatedDescription": "아이덴티티 공급자가 이 조직에서 성공적으로 연관 해제되었습니다", + "idpUnassociateMenu": "연관 해제", + "idpDeleteAllOrgsMenu": "삭제" } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index d8bd93680..913d7ca94 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -898,6 +898,7 @@ "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", "idpAutoProvisionUsers": "Automatisk brukerklargjøring", "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", + "idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisjonsinnstillingene når identitetsleverandøren er opprettet.", "licenseBadge": "EE", "idpType": "Leverandørtype", "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Standard rolletilordning", "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", "defaultMappingsOrg": "Standard organisasjonstilordning", - "defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.", + "defaultMappingsOrgDescription": "Når denne er satt, må uttrykket returnere organisasjons-IDen eller «true» for at brukeren skal få tilgang til den organisasjonen. Når den ikke er satt, er det nok å definere en rolletilordning: brukeren gis tilgang så lenge en gyldig rolletilknytting kan løses for dem i organisasjonen.", "defaultMappingsSubmit": "Lagre standard tilordninger", "orgPoliciesEdit": "Rediger Organisasjonspolicy", "org": "Organisasjon", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Internasjonalt domene oppdaget", "willbestoredas": "Vil bli lagret som:", - "roleMappingDescription": "Bestem hvordan roller tilordnes brukere når innloggingen er aktivert når autog-rapportering er aktivert.", + "roleMappingDescription": "Bestem hvordan roller tildeles brukere når de logger inn med denne identitetsleverandøren.", "selectRole": "Velg en rolle", "roleMappingExpression": "Uttrykk", "selectRolePlaceholder": "Velg en rolle", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Målet er oppdatert", "httpDestCreatedSuccess": "Målet er opprettet", "httpDestUpdateFailed": "Kunne ikke oppdatere destinasjon", - "httpDestCreateFailed": "Kan ikke opprette mål" + "httpDestCreateFailed": "Kan ikke opprette mål", + "idpAddActionCreateNew": "Opprett ny identitetsleverandør", + "idpAddActionImportFromOrg": "Importer fra en annen organisasjon", + "idpImportDialogTitle": "Importer identitetsleverandør", + "idpImportDialogDescription": "Velg en identitetsleverandør fra en organisasjon der du er admin. Den vil bli knyttet til denne organisasjonen.", + "idpImportSearchPlaceholder": "Søk etter organisasjons- eller leverandørnavn...", + "idpImportEmpty": "Ingen identitetsleverandører funnet.", + "idpImportedDescription": "Identitetsleverandøren ble importert vellykket.", + "idpDeleteGlobalQuestion": "Er du sikker på at du vil slette denne identitetsleverandøren permanent?", + "idpDeleteGlobalDescription": "Dette vil slette identitetsleverandøren permanent fra alle organisasjoner den er tilknyttet.", + "idpUnassociateTitle": "Frakoble identitetsleverandør", + "idpUnassociateQuestion": "Er du sikker på at du vil frakoble denne identitetsleverandøren fra denne organisasjonen?", + "idpUnassociateDescription": "Alle brukere knyttet til denne identitetsleverandøren vil bli fjernet fra denne organisasjonen, men identitetsleverandøren vil fortsatt eksistere for andre tilknyttede organisasjoner.", + "idpUnassociateConfirm": "Bekreft frakobling av identitetsleverandør", + "idpUnassociateWarning": "Dette kan ikke angres for denne organisasjonen.", + "idpUnassociatedDescription": "Identitetsleverandør er vellykket frakoblet fra denne organisasjonen", + "idpUnassociateMenu": "Frakoble", + "idpDeleteAllOrgsMenu": "Slett" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index fce7c836f..f3803d445 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -898,6 +898,7 @@ "idpDisplayName": "Een weergavenaam voor deze identiteitsprovider", "idpAutoProvisionUsers": "Auto Provisie Gebruikers", "idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.", + "idpAutoProvisionConfigureAfterCreate": "U kunt automatische voorzieningsinstellingen configureren zodra de identiteitsprovider is aangemaakt.", "licenseBadge": "EE", "idpType": "Type provider", "idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Standaard Rol Toewijzing", "defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.", "defaultMappingsOrg": "Standaard organisatie mapping", - "defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.", + "defaultMappingsOrgDescription": "Wanneer ingesteld, moet deze expressie de organisatie-ID of waar retourneren voor de gebruiker om toegang te krijgen tot die organisatie. Als het niet is ingesteld, is het definiëren van een roltoewijzing voldoende: de gebruiker is toegestaan zolang een geldige roltoewijzing voor hen binnen de organisatie kan worden opgelost.", "defaultMappingsSubmit": "Standaard toewijzingen opslaan", "orgPoliciesEdit": "Organisatie beleid bewerken", "org": "Organisatie", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Internationaal Domein Gedetecteerd", "willbestoredas": "Zal worden opgeslagen als:", - "roleMappingDescription": "Bepaal hoe rollen worden toegewezen aan gebruikers wanneer ze inloggen wanneer Auto Provision is ingeschakeld.", + "roleMappingDescription": "Bepaal hoe rollen aan gebruikers worden toegewezen wanneer ze zich aanmelden met deze identiteitsprovider.", "selectRole": "Selecteer een rol", "roleMappingExpression": "Expressie", "selectRolePlaceholder": "Kies een rol", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt", "httpDestCreatedSuccess": "Bestemming succesvol aangemaakt", "httpDestUpdateFailed": "Bijwerken bestemming mislukt", - "httpDestCreateFailed": "Aanmaken bestemming mislukt" + "httpDestCreateFailed": "Aanmaken bestemming mislukt", + "idpAddActionCreateNew": "Nieuwe identiteitsprovider aanmaken", + "idpAddActionImportFromOrg": "Importeer vanuit een andere organisatie", + "idpImportDialogTitle": "Importeer Identiteitsprovider", + "idpImportDialogDescription": "Kies een identiteitsprovider van een organisatie waar u beheerder bent. Het wordt gekoppeld aan deze organisatie.", + "idpImportSearchPlaceholder": "Zoek op organisatie- of providernamen...", + "idpImportEmpty": "Geen identiteitsproviders gevonden.", + "idpImportedDescription": "Identiteitsprovider succesvol geïmporteerd.", + "idpDeleteGlobalQuestion": "Weet u zeker dat u deze identiteitsprovider permanent wilt verwijderen?", + "idpDeleteGlobalDescription": "Hiermee wordt de identiteitsprovider permanent verwijderd uit alle organisaties waarmee het is geassocieerd.", + "idpUnassociateTitle": "Koppel Identiteitsprovider los", + "idpUnassociateQuestion": "Weet u zeker dat u deze identiteitsprovider van deze organisatie wilt loskoppelen?", + "idpUnassociateDescription": "Alle gebruikers die aan deze identiteitsprovider zijn gekoppeld, worden uit deze organisatie verwijderd, maar de identiteitsprovider blijft bestaan voor andere gerelateerde organisaties.", + "idpUnassociateConfirm": "Bevestig ontkoppelen identiteitsprovider", + "idpUnassociateWarning": "Dit kan niet ongedaan worden gemaakt voor deze organisatie.", + "idpUnassociatedDescription": "Identiteitsprovider succesvol losgekoppeld van deze organisatie", + "idpUnassociateMenu": "Ontkoppelen", + "idpDeleteAllOrgsMenu": "Verwijderen" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 41b10b7fb..2e55ad2a8 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -898,6 +898,7 @@ "idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości", "idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników", "idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.", + "idpAutoProvisionConfigureAfterCreate": "Możesz skonfigurować automatyczne ustawienia provision, gdy dostawca tożsamości zostanie utworzony.", "licenseBadge": "EE", "idpType": "Typ dostawcy", "idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Domyślne mapowanie roli", "defaultMappingsRoleDescription": "JMESPath do wydobycia informacji o roli z tokena ID. Wynik tego wyrażenia musi zwrócić nazwę roli zdefiniowaną w organizacji jako ciąg znaków.", "defaultMappingsOrg": "Domyślne mapowanie organizacji", - "defaultMappingsOrgDescription": "JMESPath do wydobycia informacji o organizacji z tokena ID. To wyrażenie musi zwrócić ID organizacji lub true, aby użytkownik mógł uzyskać dostęp do organizacji.", + "defaultMappingsOrgDescription": "Gdy jest ustawiona, ta wyrażenie musi zwrócić identyfikator organizacji lub true, aby użytkownik mógł uzyskać dostęp do tej organizacji. Gdy nie jest ustawiona, wystarczające jest zdefiniowanie mapowania ról: użytkownik jest dopuszczony, o ile można rozwiązać dla niego ważne mapowanie ról w organizacji.", "defaultMappingsSubmit": "Zapisz domyślne mapowania", "orgPoliciesEdit": "Edytuj politykę organizacji", "org": "Organizacja", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Wykryto międzynarodową domenę", "willbestoredas": "Będą przechowywane jako:", - "roleMappingDescription": "Określ jak role są przypisywane do użytkowników podczas logowania się, gdy automatyczne świadczenie jest włączone.", + "roleMappingDescription": "Określ, jak role są przypisywane użytkownikom podczas logowania się z tym dostawcą tożsamości.", "selectRole": "Wybierz rolę", "roleMappingExpression": "Wyrażenie", "selectRolePlaceholder": "Wybierz rolę", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Cel został pomyślnie zaktualizowany", "httpDestCreatedSuccess": "Cel został utworzony pomyślnie", "httpDestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego", - "httpDestCreateFailed": "Nie udało się utworzyć miejsca docelowego" + "httpDestCreateFailed": "Nie udało się utworzyć miejsca docelowego", + "idpAddActionCreateNew": "Utwórz nowego dostawcę tożsamości", + "idpAddActionImportFromOrg": "Importuj z innej organizacji", + "idpImportDialogTitle": "Importuj dostawcę tożsamości", + "idpImportDialogDescription": "Wybierz dostawcę tożsamości z organizacji, w której jesteś administratorem. Zostanie on powiązany z tą organizacją.", + "idpImportSearchPlaceholder": "Szukaj według nazwy organizacji lub dostawcy...", + "idpImportEmpty": "Nie znaleziono dostawców tożsamości.", + "idpImportedDescription": "Dostawca tożsamości został pomyślnie zaimportowany.", + "idpDeleteGlobalQuestion": "Czy na pewno chcesz trwale usunąć tego dostawcę tożsamości?", + "idpDeleteGlobalDescription": "Spowoduje to trwałe usunięcie dostawcy tożsamości ze wszystkich organizacji, z którymi jest powiązany.", + "idpUnassociateTitle": "Odłącz dostawcę tożsamości", + "idpUnassociateQuestion": "Czy na pewno chcesz odłączyć tego dostawcę tożsamości od tej organizacji?", + "idpUnassociateDescription": "Wszystkie użytkownicy powiązani z tym dostawcą tożsamości zostaną usunięci z tej organizacji, ale dostawca tożsamości będzie nadal istniał dla innych powiązanych organizacji.", + "idpUnassociateConfirm": "Potwierdź odłączenie dostawcy tożsamości", + "idpUnassociateWarning": "Tego nie można cofnąć dla tej organizacji.", + "idpUnassociatedDescription": "Dostawca tożsamości pomyślnie odłączony od tej organizacji", + "idpUnassociateMenu": "Odłącz", + "idpDeleteAllOrgsMenu": "Usuń" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index df7ef9f17..2fa228639 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -898,6 +898,7 @@ "idpDisplayName": "Um nome de exibição para este provedor de identidade", "idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores", "idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.", + "idpAutoProvisionConfigureAfterCreate": "Você pode configurar as definições de auto provisão assim que o provedor de identidade for criado.", "licenseBadge": "EE", "idpType": "Tipo de Provedor", "idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Mapeamento de Função Padrão", "defaultMappingsRoleDescription": "JMESPath para extrair informações de função do token ID. O resultado desta expressão deve retornar o nome da função como definido na organização como uma string.", "defaultMappingsOrg": "Mapeamento de Organização Padrão", - "defaultMappingsOrgDescription": "JMESPath para extrair informações da organização do token ID. Esta expressão deve retornar o ID da organização ou verdadeiro para que o utilizador tenha permissão para aceder à organização.", + "defaultMappingsOrgDescription": "Quando definida, esta expressão deve retornar o ID da organização ou verdadeiro para que o usuário acesse essa organização. Quando não definida, a definição de um mapeamento de papel é suficiente: o usuário é permitido desde que um mapeamento de papel válido possa ser resolvido para ele dentro da organização.", "defaultMappingsSubmit": "Guardar Mapeamentos Padrão", "orgPoliciesEdit": "Editar Política da Organização", "org": "Organização", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Domínio Internacional Detectado", "willbestoredas": "Será armazenado como:", - "roleMappingDescription": "Determinar como as funções são atribuídas aos usuários quando eles fazem login quando Auto Provisão está habilitada.", + "roleMappingDescription": "Determine como os papéis são atribuídos aos usuários quando eles entram com este provedor de identidade.", "selectRole": "Selecione uma função", "roleMappingExpression": "Expressão", "selectRolePlaceholder": "Escolha uma função", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Destino atualizado com sucesso", "httpDestCreatedSuccess": "Destino criado com sucesso", "httpDestUpdateFailed": "Falha ao atualizar destino", - "httpDestCreateFailed": "Falha ao criar destino" + "httpDestCreateFailed": "Falha ao criar destino", + "idpAddActionCreateNew": "Criar novo provedor de identidade", + "idpAddActionImportFromOrg": "Importar de outra organização", + "idpImportDialogTitle": "Importar Provedor de Identidade", + "idpImportDialogDescription": "Escolha um provedor de identidade de uma organização onde você é administrador. Ele será vinculado a esta organização.", + "idpImportSearchPlaceholder": "Pesquisar por nome de organização ou provedor...", + "idpImportEmpty": "Nenhum provedor de identidade encontrado.", + "idpImportedDescription": "Provedor de identidade importado com sucesso.", + "idpDeleteGlobalQuestion": "Tem certeza de que deseja eliminar permanentemente este provedor de identidade?", + "idpDeleteGlobalDescription": "Isso eliminará permanentemente o provedor de identidade de todas as organizações com as quais está associado.", + "idpUnassociateTitle": "Desassociar Provedor de Identidade", + "idpUnassociateQuestion": "Tem certeza de que deseja desassociar este provedor de identidade desta organização?", + "idpUnassociateDescription": "Todos os usuários associados a este provedor de identidade serão removidos desta organização, mas o provedor de identidade continuará a existir para outras organizações associadas.", + "idpUnassociateConfirm": "Confirmar Desassociação do Provedor de Identidade", + "idpUnassociateWarning": "Isso não pode ser desfeito para esta organização.", + "idpUnassociatedDescription": "Provedor de identidade desassociado desta organização com sucesso", + "idpUnassociateMenu": "Desassociar", + "idpDeleteAllOrgsMenu": "Excluir" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 7a8bee8b9..871b292d9 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -898,6 +898,7 @@ "idpDisplayName": "Отображаемое имя для этого поставщика удостоверений", "idpAutoProvisionUsers": "Автоматическое создание пользователей", "idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.", + "idpAutoProvisionConfigureAfterCreate": "Вы можете настроить параметры автоматического обеспечения после создания поставщика удостоверений.", "licenseBadge": "EE", "idpType": "Тип поставщика", "idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Сопоставление ролей по умолчанию", "defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.", "defaultMappingsOrg": "Сопоставление организаций по умолчанию", - "defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.", + "defaultMappingsOrgDescription": "При установке это выражение должно возвращать ID организации или true, чтобы пользователь мог получить доступ к этой организации. При отсутствии настройка отображения роли достаточно: пользователю разрешено войти, пока для него может быть решено отображение гарантированной роли в организации.", "defaultMappingsSubmit": "Сохранить сопоставления по умолчанию", "orgPoliciesEdit": "Редактировать политику организации", "org": "Организация", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Обнаружен международный домен", "willbestoredas": "Будет храниться как:", - "roleMappingDescription": "Определите, как роли, назначаемые пользователям, когда они войдут в систему автоматического профиля.", + "roleMappingDescription": "Определите, как роли присваиваются пользователям при входе с этим поставщиком удостоверений.", "selectRole": "Выберите роль", "roleMappingExpression": "Выражение", "selectRolePlaceholder": "Выберите роль", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Адрес назначения успешно обновлен", "httpDestCreatedSuccess": "Адрес назначения успешно создан", "httpDestUpdateFailed": "Не удалось обновить место назначения", - "httpDestCreateFailed": "Не удалось создать место назначения" + "httpDestCreateFailed": "Не удалось создать место назначения", + "idpAddActionCreateNew": "Создать нового поставщика удостоверений", + "idpAddActionImportFromOrg": "Импортировать из другой организации", + "idpImportDialogTitle": "Импортировать поставщика удостоверений", + "idpImportDialogDescription": "Выберите поставщика удостоверений из организации, где вы являетесь администратором. Он будет связан с этой организацией.", + "idpImportSearchPlaceholder": "Поиск по организации или имени поставщика...", + "idpImportEmpty": "Поставщики удостоверений не найдены.", + "idpImportedDescription": "Поставщик удостоверений успешно импортирован.", + "idpDeleteGlobalQuestion": "Вы уверены, что хотите навсегда удалить этого поставщика удостоверений?", + "idpDeleteGlobalDescription": "Это навсегда удалит поставщика удостоверений из всех организаций, с которыми он связан.", + "idpUnassociateTitle": "Рассоединить провайдера удостоверений", + "idpUnassociateQuestion": "Вы уверены, что хотите рассоединить этого поставщика удостоверений с этой организацией?", + "idpUnassociateDescription": "Все пользователи, связанные с этим поставщиком удостоверений, будут удалены из этой организации, но поставщик удостоверений будет продолжать существовать для других связанных организаций.", + "idpUnassociateConfirm": "Подтвердите рассоединение поставщика удостоверений", + "idpUnassociateWarning": "Это не может быть отменено для этой организации.", + "idpUnassociatedDescription": "Поставщик удостоверений успешно рассоединен с этой организацией", + "idpUnassociateMenu": "Рассоединить", + "idpDeleteAllOrgsMenu": "Удалить" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 68de3399d..754b529ac 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -898,6 +898,7 @@ "idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı", "idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla", "idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.", + "idpAutoProvisionConfigureAfterCreate": "Kimlik sağlayıcı oluşturulduktan sonra otomatik sağlama ayarlarını yapılandırabilirsiniz.", "licenseBadge": " ", "idpType": "Sağlayıcı Türü", "idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin", @@ -949,7 +950,7 @@ "defaultMappingsRole": "Varsayılan Rol Eşleme", "defaultMappingsRoleDescription": "JMESPath to extract role information from the ID token. The result of this expression must return the role name as defined in the organization as a string.", "defaultMappingsOrg": "Varsayılan Kuruluş Eşleme", - "defaultMappingsOrgDescription": "JMESPath to extract organization information from the ID token. This expression must return the org ID or true for the user to be allowed to access the organization.", + "defaultMappingsOrgDescription": "Ayarladığınızda, bu ifade kullanıcının o kuruluşa erişmesi için kuruluş kimliğini veya doğru değerini döndürmelidir. Ayarlamadığınızda, rol eşleme tanımlamak yeterlidir: kullanıcı, kuruluş içinde onlar için geçerli bir rol eşlemesi çözümlenebildiği sürece erişime izin verilir.", "defaultMappingsSubmit": "Varsayılan Eşlemeleri Kaydet", "orgPoliciesEdit": "Kuruluş Politikasını Düzenle", "org": "Kuruluş", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi", "willbestoredas": "Şu şekilde depolanacak:", - "roleMappingDescription": "Otomatik Sağlama etkinleştirildiğinde kullanıcıların oturum açarken rollerin nasıl atandığını belirleyin.", + "roleMappingDescription": "Bu kimlik sağlayıcı ile oturum açıldığında kullanıcılara rollerin nasıl atandığını belirleyin.", "selectRole": "Bir Rol Seçin", "roleMappingExpression": "İfade", "selectRolePlaceholder": "Bir rol seçin", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "Hedef başarıyla güncellendi", "httpDestCreatedSuccess": "Hedef başarıyla oluşturuldu", "httpDestUpdateFailed": "Hedef güncellenemedi", - "httpDestCreateFailed": "Hedef oluşturulamadı" + "httpDestCreateFailed": "Hedef oluşturulamadı", + "idpAddActionCreateNew": "Yeni kimlik sağlayıcı oluştur", + "idpAddActionImportFromOrg": "Başka bir kuruluştan içe aktar", + "idpImportDialogTitle": "Kimlik Sağlayıcı İçe Aktar", + "idpImportDialogDescription": "Bir kuruluştan yönetici olduğunuz bir kimlik sağlayıcı seçin. Bu kuruluşla ilişkilendirilecektir.", + "idpImportSearchPlaceholder": "Kuruluş veya sağlayıcı adına göre ara...", + "idpImportEmpty": "Hiçbir kimlik sağlayıcı bulunamadı.", + "idpImportedDescription": "Kimlik sağlayıcı başarıyla içe aktarıldı.", + "idpDeleteGlobalQuestion": "Bu kimlik sağlayıcıyı kalıcı olarak silmek istediğinizden emin misiniz?", + "idpDeleteGlobalDescription": "Bu, kimlik sağlayıcıyı ilişkilendirildiği tüm kuruluşlardan kalıcı olarak silecektir.", + "idpUnassociateTitle": "Kimlik Sağlayıcının İlişkisini Kes", + "idpUnassociateQuestion": "Bu kimlik sağlayıcının bu kuruluştan ilişiğini kesmek istediğinizden emin misiniz?", + "idpUnassociateDescription": "Bu kimlik sağlayıcı ile ilişkilendirilen tüm kullanıcılar bu kuruluştan kaldırılacaktır, ancak kimlik sağlayıcı diğer ilişkilendirilen kuruluşlar için var olmaya devam edecektir.", + "idpUnassociateConfirm": "Kimlik Sağlayıcının İlişkisinin Kesilmesini Onayla", + "idpUnassociateWarning": "Bu işlem bu kuruluş için geri alınamaz.", + "idpUnassociatedDescription": "Kimlik sağlayıcı bu kuruluştan başarıyla ayrıldı", + "idpUnassociateMenu": "İlişkiyi Kes", + "idpDeleteAllOrgsMenu": "Sil" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 955ad7096..038d4cb01 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -898,6 +898,7 @@ "idpDisplayName": "此身份提供商的显示名称", "idpAutoProvisionUsers": "自动提供用户", "idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。", + "idpAutoProvisionConfigureAfterCreate": "您可以在创建身份提供者后配置自动配置设置。", "licenseBadge": "EE", "idpType": "提供者类型", "idpTypeDescription": "选择您想要配置的身份提供者类型", @@ -949,7 +950,7 @@ "defaultMappingsRole": "默认角色映射", "defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。", "defaultMappingsOrg": "默认组织映射", - "defaultMappingsOrgDescription": "此表达式必须返回 组织ID 或 true 才能允许用户访问组织。", + "defaultMappingsOrgDescription": "设置时,此表达式必须返回组织ID或true才能让用户访问该组织。如果未设置,定义角色映射就足够了:只要在组织内可以为用户找出有效角色映射,用户就被允许进入。", "defaultMappingsSubmit": "保存默认映射", "orgPoliciesEdit": "编辑组织策略", "org": "组织", @@ -2026,7 +2027,7 @@ }, "internationaldomaindetected": "检测到国际域", "willbestoredas": "储存为:", - "roleMappingDescription": "确定当用户启用自动配送时如何分配他们的角色。", + "roleMappingDescription": "确定当用户使用此身份提供者登陆时如何分配角色。", "selectRole": "选择角色", "roleMappingExpression": "表达式", "selectRolePlaceholder": "选择角色", @@ -2899,5 +2900,22 @@ "httpDestUpdatedSuccess": "目标已成功更新", "httpDestCreatedSuccess": "目标创建成功", "httpDestUpdateFailed": "更新目标失败", - "httpDestCreateFailed": "创建目标失败" + "httpDestCreateFailed": "创建目标失败", + "idpAddActionCreateNew": "创建新的身份提供者", + "idpAddActionImportFromOrg": "从另一个组织导入", + "idpImportDialogTitle": "导入身份提供者", + "idpImportDialogDescription": "从您是管理员的组织中选择一个身份提供者。它将关联到本组织。", + "idpImportSearchPlaceholder": "按组织或提供者名称搜索……", + "idpImportEmpty": "未找到身份提供者。", + "idpImportedDescription": "身份提供者已成功导入。", + "idpDeleteGlobalQuestion": "您确定要永久删除此身份提供者吗?", + "idpDeleteGlobalDescription": "这将永久删除与其关联的所有组织中的身份提供者。", + "idpUnassociateTitle": "取消关联身份提供者", + "idpUnassociateQuestion": "您确定要将此身份提供者从此组织中取消关联吗?", + "idpUnassociateDescription": "与此身份提供者关联的所有用户将从该组织中移除,但身份提供者仍会继续存在于关联的其他组织中。", + "idpUnassociateConfirm": "确认取消关联身份提供者", + "idpUnassociateWarning": "此操作无法对该组织撤销。", + "idpUnassociatedDescription": "身份提供者已成功从该组织中取消关联", + "idpUnassociateMenu": "取消关联", + "idpDeleteAllOrgsMenu": "删除" } diff --git a/public/idp/openid.png b/public/idp/openid.png new file mode 100644 index 000000000..d4422c872 Binary files /dev/null and b/public/idp/openid.png differ diff --git a/public/screenshots/hero.png b/public/screenshots/hero.png index c33d2924b..918dd755d 100644 Binary files a/public/screenshots/hero.png and b/public/screenshots/hero.png differ diff --git a/public/screenshots/private-resources.png b/public/screenshots/private-resources.png index 7e5b05f40..4a4b50d4e 100644 Binary files a/public/screenshots/private-resources.png and b/public/screenshots/private-resources.png differ diff --git a/public/screenshots/public-resources.png b/public/screenshots/public-resources.png index c33d2924b..918dd755d 100644 Binary files a/public/screenshots/public-resources.png and b/public/screenshots/public-resources.png differ diff --git a/public/screenshots/sites.png b/public/screenshots/sites.png index fae86ceeb..f65707bce 100644 Binary files a/public/screenshots/sites.png and b/public/screenshots/sites.png differ diff --git a/public/screenshots/users.png b/public/screenshots/users.png index 3b47e8bbc..69be0452f 100644 Binary files a/public/screenshots/users.png and b/public/screenshots/users.png differ diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index f7a4c71ab..159ee2449 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -42,7 +42,9 @@ import { verifyRoleAccess, verifyUserAccess, verifyUserCanSetUserOrgRoles, - verifySiteProvisioningKeyAccess + verifySiteProvisioningKeyAccess, + verifyIsLoggedInUser, + verifyAdmin } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import { @@ -89,6 +91,7 @@ authenticated.put( "/org/:orgId/idp/oidc", verifyValidLicense, verifyValidSubscription(tierMatrix.orgOidc), + orgIdp.requireOrgIdentityProviderMode, verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.createIdp), @@ -96,10 +99,23 @@ authenticated.put( orgIdp.createOrgOidcIdp ); +authenticated.post( + "/org/:orgId/idp/:idpId/import", + verifyValidLicense, + verifyValidSubscription(tierMatrix.orgOidc), + orgIdp.requireOrgIdentityProviderMode, + verifyOrgAccess, + verifyLimits, + verifyAdmin, + logActionAudit(ActionsEnum.createIdp), + orgIdp.importOrgIdp +); + authenticated.post( "/org/:orgId/idp/:idpId/oidc", verifyValidLicense, verifyValidSubscription(tierMatrix.orgOidc), + orgIdp.requireOrgIdentityProviderMode, verifyOrgAccess, verifyIdpAccess, verifyLimits, @@ -111,6 +127,7 @@ authenticated.post( authenticated.delete( "/org/:orgId/idp/:idpId", verifyValidLicense, + orgIdp.requireOrgIdentityProviderMode, verifyOrgAccess, verifyIdpAccess, verifyUserHasAction(ActionsEnum.deleteIdp), @@ -118,6 +135,17 @@ authenticated.delete( orgIdp.deleteOrgIdp ); +authenticated.delete( + "/org/:orgId/idp/:idpId/association", + verifyValidLicense, + orgIdp.requireOrgIdentityProviderMode, + verifyOrgAccess, + verifyIdpAccess, + verifyUserHasAction(ActionsEnum.deleteIdp), + logActionAudit(ActionsEnum.deleteIdp), + orgIdp.unassociateOrgIdp +); + authenticated.get( "/org/:orgId/idp/:idpId", verifyValidLicense, @@ -127,16 +155,14 @@ authenticated.get( orgIdp.getOrgIdp ); -authenticated.get( - "/org/:orgId/idp", - verifyValidLicense, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.listIdps), - orgIdp.listOrgIdps -); - authenticated.get("/org/:orgId/idp", orgIdp.listOrgIdps); // anyone can see this; it's just a list of idp names and ids +authenticated.get( + "/user/:userId/admin-org-idps", + verifyIsLoggedInUser, + orgIdp.listUserAdminOrgIdps +); + authenticated.get( "/org/:orgId/certificate/:domainId/:domain", verifyValidLicense, diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index b14348a2a..97928d99f 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -27,7 +27,6 @@ import config from "@server/lib/config"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { isSubscribed } from "#private/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import privateConfig from "#private/lib/config"; import { build } from "@server/build"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); @@ -45,6 +44,7 @@ const bodySchema = z.strictObject({ autoProvision: z.boolean().optional(), variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"), roleMapping: z.string().optional(), + orgMapping: z.string().nullish(), tags: z.string().optional() }); @@ -94,18 +94,6 @@ export async function createOrgOidcIdp( ); } - if ( - privateConfig.getRawPrivateConfig().app.identity_provider_mode !== - "org" - ) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." - ) - ); - } - const { clientId, clientSecret, @@ -118,6 +106,7 @@ export async function createOrgOidcIdp( name, variant, roleMapping, + orgMapping: orgMappingBody, tags } = parsedBody.data; @@ -169,7 +158,7 @@ export async function createOrgOidcIdp( idpId: idpRes.idpId, orgId: orgId, roleMapping: roleMapping || null, - orgMapping: `'${orgId}'` + orgMapping: orgMappingBody }); }); diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 304826cd1..9e5dfccee 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -22,7 +22,6 @@ import { fromError } from "zod-validation-error"; import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import privateConfig from "#private/lib/config"; const paramsSchema = z .object({ @@ -60,18 +59,6 @@ export async function deleteOrgIdp( const { idpId } = parsedParams.data; - if ( - privateConfig.getRawPrivateConfig().app.identity_provider_mode !== - "org" - ) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." - ) - ); - } - // Check if IDP exists const [existingIdp] = await db .select() diff --git a/server/private/routers/orgIdp/importOrgIdp.ts b/server/private/routers/orgIdp/importOrgIdp.ts new file mode 100644 index 000000000..1f4f5ddd9 --- /dev/null +++ b/server/private/routers/orgIdp/importOrgIdp.ts @@ -0,0 +1,211 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { idp, idpOrg, orgs, roles, userOrgs } from "@server/db"; +import { and, eq, inArray } from "drizzle-orm"; +import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; +import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; +import { checkOrgAccessPolicy } from "#private/lib/checkOrgAccessPolicy"; +import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty(), + idpId: z.coerce.number().int().positive() +}); + +const bodySchema = z.strictObject({ + sourceOrgId: z.string().nonempty() +}); + +async function userIsOrgAdmin( + userId: string, + orgId: string, + session: Request["session"] +): Promise { + const [userOrgRow] = await db + .select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (!userOrgRow) { + return false; + } + + const policyCheck = await checkOrgAccessPolicy({ + orgId, + userId, + session + }); + if (!policyCheck.allowed || policyCheck.error) { + return false; + } + + const roleIds = await getUserOrgRoleIds(userId, orgId); + if (roleIds.length === 0) { + return false; + } + + const [adminRole] = await db + .select() + .from(roles) + .where(and(inArray(roles.roleId, roleIds), eq(roles.isAdmin, true))) + .limit(1); + + return !!adminRole; +} + +export async function importOrgIdp( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId: targetOrgId, idpId } = parsedParams.data; + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { sourceOrgId } = parsedBody.data; + + if (sourceOrgId === targetOrgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Source and target organization must be different" + ) + ); + } + + const userId = req.user!.userId; + + const sourceLinked = await db + .select() + .from(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, sourceOrgId))) + .limit(1); + + if (sourceLinked.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "IdP not found for the source organization" + ) + ); + } + + const sourceAdmin = await userIsOrgAdmin( + userId, + sourceOrgId, + req.session + ); + if (!sourceAdmin) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "You must be an organization admin in the source organization where this IdP is linked" + ) + ); + } + + const [targetOrg] = await db + .select({ orgId: orgs.orgId }) + .from(orgs) + .where(eq(orgs.orgId, targetOrgId)) + .limit(1); + + if (!targetOrg) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Target organization not found" + ) + ); + } + + const [existingIdp] = await db + .select() + .from(idp) + .where(eq(idp.idpId, idpId)) + .limit(1); + + if (!existingIdp) { + return next(createHttpError(HttpCode.NOT_FOUND, "IdP not found")); + } + + const alreadyTarget = await db + .select() + .from(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, targetOrgId))) + .limit(1); + + if (alreadyTarget.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "This IdP is already linked to the target organization" + ) + ); + } + + await db.insert(idpOrg).values({ + idpId, + orgId: targetOrgId, + roleMapping: null, + orgMapping: null + }); + + const redirectUrl = await generateOidcRedirectUrl(idpId, targetOrgId); + + return response(res, { + data: { + idpId, + redirectUrl + }, + success: true, + error: false, + message: "Org IdP imported successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/orgIdp/index.ts b/server/private/routers/orgIdp/index.ts index e3f967f86..192d883a6 100644 --- a/server/private/routers/orgIdp/index.ts +++ b/server/private/routers/orgIdp/index.ts @@ -12,7 +12,11 @@ */ export * from "./createOrgOidcIdp"; +export * from "./importOrgIdp"; export * from "./getOrgIdp"; export * from "./listOrgIdps"; +export * from "./listUserAdminOrgIdps"; export * from "./updateOrgOidcIdp"; export * from "./deleteOrgIdp"; +export * from "./unassociateOrgIdp"; +export * from "./requireOrgIdentityProviderMode"; diff --git a/server/private/routers/orgIdp/listUserAdminOrgIdps.ts b/server/private/routers/orgIdp/listUserAdminOrgIdps.ts new file mode 100644 index 000000000..78faa48fa --- /dev/null +++ b/server/private/routers/orgIdp/listUserAdminOrgIdps.ts @@ -0,0 +1,160 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, idpOidcConfig } from "@server/db"; +import { idp, idpOrg, orgs, roles, userOrgRoles } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { and, eq, inArray, sql } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { ListUserAdminOrgIdpsResponse } from "@server/routers/orgIdp/types"; + +const querySchema = z.strictObject({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); + +const paramsSchema = z.strictObject({ + userId: z.string().nonempty() +}); + +async function getOrgIdsWhereUserIsAdmin(userId: string): Promise { + const rows = await db + .select({ orgId: userOrgRoles.orgId }) + .from(userOrgRoles) + .innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId)) + .where(and(eq(userOrgRoles.userId, userId), eq(roles.isAdmin, true))); + return [...new Set(rows.map((r) => r.orgId))]; +} + +async function queryIdpsForOrgs( + orgIds: string[], + limit: number, + offset: number +) { + return db + .select({ + idpId: idp.idpId, + orgId: idpOrg.orgId, + orgName: orgs.name, + name: idp.name, + type: idp.type, + variant: idpOidcConfig.variant, + tags: idp.tags + }) + .from(idpOrg) + .where(inArray(idpOrg.orgId, orgIds)) + .innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId)) + .innerJoin(idp, eq(idp.idpId, idpOrg.idpId)) + .innerJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idpOrg.idpId)) + .orderBy(sql`idp.name DESC`) + .limit(limit) + .offset(offset); +} + +async function countIdpsForOrgs(orgIds: string[]) { + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(idpOrg) + .innerJoin(idp, eq(idp.idpId, idpOrg.idpId)) + .innerJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idpOrg.idpId)) + .where(inArray(idpOrg.orgId, orgIds)); + return count; +} + +export async function listUserAdminOrgIdps( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + const { userId } = parsedParams.data; + + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const adminOrgIds = await getOrgIdsWhereUserIsAdmin(userId); + + if (adminOrgIds.length === 0) { + return response(res, { + data: { + idps: [], + pagination: { + total: 0, + limit, + offset + } + }, + success: true, + error: false, + message: "Org Idps retrieved successfully", + status: HttpCode.OK + }); + } + + const list = await queryIdpsForOrgs(adminOrgIds, limit, offset); + const total = await countIdpsForOrgs(adminOrgIds); + + return response(res, { + data: { + idps: list, + pagination: { + total, + limit, + offset + } + }, + success: true, + error: false, + message: "Org Idps retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/orgIdp/requireOrgIdentityProviderMode.ts b/server/private/routers/orgIdp/requireOrgIdentityProviderMode.ts new file mode 100644 index 000000000..7942af123 --- /dev/null +++ b/server/private/routers/orgIdp/requireOrgIdentityProviderMode.ts @@ -0,0 +1,34 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import privateConfig from "#private/lib/config"; +import HttpCode from "@server/types/HttpCode"; + +export function requireOrgIdentityProviderMode( + _req: Request, + _res: Response, + next: NextFunction +): void { + if (privateConfig.getRawPrivateConfig().app.identity_provider_mode !== "org") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." + ) + ); + } + + return next(); +} diff --git a/server/private/routers/orgIdp/unassociateOrgIdp.ts b/server/private/routers/orgIdp/unassociateOrgIdp.ts new file mode 100644 index 000000000..f6ab557b3 --- /dev/null +++ b/server/private/routers/orgIdp/unassociateOrgIdp.ts @@ -0,0 +1,96 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, idpOrg } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { and, eq, sql } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + orgId: z.string().nonempty(), + idpId: z.coerce.number().int().positive() + }) + .strict(); + +export async function unassociateOrgIdp( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, idpId } = parsedParams.data; + + const [association] = await db + .select() + .from(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))) + .limit(1); + + if (!association) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `IdP with ID ${idpId} is not associated with organization ${orgId}` + ) + ); + } + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + if (count <= 1) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "This is the last organization associated with this identity provider. Delete it instead." + ) + ); + } + + await db + .delete(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); + + return response(res, { + data: null, + success: true, + error: false, + message: "Org IdP unassociated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index 17bf2ee35..7c379f8ec 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -26,7 +26,6 @@ import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; import { isSubscribed } from "#private/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import privateConfig from "#private/lib/config"; import { build } from "@server/build"; const paramsSchema = z @@ -48,6 +47,7 @@ const bodySchema = z.strictObject({ scopes: z.string().optional(), autoProvision: z.boolean().optional(), roleMapping: z.string().optional(), + orgMapping: z.string().nullish(), tags: z.string().optional() }); @@ -99,18 +99,6 @@ export async function updateOrgOidcIdp( ); } - if ( - privateConfig.getRawPrivateConfig().app.identity_provider_mode !== - "org" - ) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." - ) - ); - } - const { idpId, orgId } = parsedParams.data; const { clientId, @@ -123,6 +111,7 @@ export async function updateOrgOidcIdp( namePath, name, roleMapping, + orgMapping, tags } = parsedBody.data; @@ -218,13 +207,20 @@ export async function updateOrgOidcIdp( .where(eq(idpOidcConfig.idpId, idpId)); } + const idpOrgPolicyPatch: { + roleMapping?: string; + orgMapping?: string | null; + } = {}; if (roleMapping !== undefined) { - // Update IdP-org policy + idpOrgPolicyPatch.roleMapping = roleMapping; + } + if (orgMapping !== undefined) { + idpOrgPolicyPatch.orgMapping = orgMapping; + } + if (Object.keys(idpOrgPolicyPatch).length > 0) { await trx .update(idpOrg) - .set({ - roleMapping - }) + .set(idpOrgPolicyPatch) .where( and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId)) ); diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts index f6f581eed..40dbb2cf4 100644 --- a/server/routers/orgIdp/types.ts +++ b/server/routers/orgIdp/types.ts @@ -25,3 +25,22 @@ export type ListOrgIdpsResponse = { offset: number; }; }; + +export type ListUserAdminOrgIdpsEntry = { + idpId: number; + orgId: string; + orgName: string; + name: string; + type: string; + variant: string; + tags: string | null; +}; + +export type ListUserAdminOrgIdpsResponse = { + idps: ListUserAdminOrgIdpsEntry[]; + pagination: { + total: number; + limit: number; + offset: number; + }; +}; diff --git a/server/setup/ensureRootApiKey.ts b/server/setup/ensureRootApiKey.ts new file mode 100644 index 000000000..4cf9c032b --- /dev/null +++ b/server/setup/ensureRootApiKey.ts @@ -0,0 +1,106 @@ +import { db, apiKeys } from "@server/db"; +import { eq } from "drizzle-orm"; +import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; +import moment from "moment"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; + +const random: RandomReader = { + read(bytes: Uint8Array): void { + crypto.getRandomValues(bytes); + } +}; + +function validateApiKeyId(id: string): boolean { + return /^[a-z0-9]{15}$/.test(id); +} + +function validateApiKeySecret(secret: string): boolean { + return secret.length > 0; +} + +function showRootApiKey(apiKeyId: string, source: string): void { + console.log(`=== ROOT API KEY ${source} ===`); + console.log("API Key ID:", apiKeyId); + console.log( + "The root API key from PANGOLIN_ROOT_API_KEY has been applied." + ); + console.log("Use the full key value (apiKeyId.apiKeySecret) in requests."); + console.log("================================"); +} + +export async function ensureRootApiKey() { + try { + const envApiKey = process.env.PANGOLIN_ROOT_API_KEY; + + if (!envApiKey) { + logger.debug( + "PANGOLIN_ROOT_API_KEY not set. Root API key from environment skipped." + ); + return; + } + + const parts = envApiKey.split("."); + if (parts.length !== 2) { + throw new Error( + "Invalid format for PANGOLIN_ROOT_API_KEY. Expected format: {apiKeyId}.{apiKeySecret}" + ); + } + + const [apiKeyId, apiKeySecret] = parts; + + if (!validateApiKeyId(apiKeyId)) { + throw new Error( + "Invalid apiKeyId in PANGOLIN_ROOT_API_KEY. Must be 15 lowercase alphanumeric characters." + ); + } + + if (!validateApiKeySecret(apiKeySecret)) { + throw new Error( + "Invalid apiKeySecret in PANGOLIN_ROOT_API_KEY. Secret must not be empty." + ); + } + + const apiKeyHash = await hashPassword(apiKeySecret); + const lastChars = apiKeySecret.slice(-4); + const createdAt = moment().toISOString(); + + const [existingKey] = await db + .select() + .from(apiKeys) + .where(eq(apiKeys.apiKeyId, apiKeyId)); + + if (existingKey) { + if (!existingKey.isRoot) { + console.warn( + `API key with ID ${apiKeyId} exists but is not a root key. Promoting to root and updating hash.` + ); + } else { + console.warn( + `Overwriting existing root API key hash since PANGOLIN_ROOT_API_KEY is set (apiKeyId: ${apiKeyId})` + ); + } + + await db + .update(apiKeys) + .set({ apiKeyHash, lastChars, isRoot: true }) + .where(eq(apiKeys.apiKeyId, apiKeyId)); + + showRootApiKey(apiKeyId, "UPDATED FROM ENVIRONMENT"); + } else { + await db.insert(apiKeys).values({ + apiKeyId, + name: "Root API Key (Environment)", + apiKeyHash, + lastChars, + createdAt, + isRoot: true + }); + + showRootApiKey(apiKeyId, "CREATED FROM ENVIRONMENT"); + } + } catch (error) { + console.error("Failed to ensure root API key:", error); + throw error; + } +} \ No newline at end of file diff --git a/server/setup/index.ts b/server/setup/index.ts index 2dfb633e5..c46e6b8fd 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -2,10 +2,12 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; import { clearStaleData } from "./clearStaleData"; import { ensureSetupToken } from "./ensureSetupToken"; +import { ensureRootApiKey } from "./ensureRootApiKey"; export async function runSetupFunctions() { await copyInConfig(); // copy in the config to the db as needed await ensureActions(); // make sure all of the actions are in the db and the roles await clearStaleData(); await ensureSetupToken(); // ensure setup token exists for initial setup + await ensureRootApiKey(); } diff --git a/src/app/[orgId]/settings/(private)/access/approvals/page.tsx b/src/app/[orgId]/settings/(private)/access/approvals/page.tsx index de69de046..7f7060b05 100644 --- a/src/app/[orgId]/settings/(private)/access/approvals/page.tsx +++ b/src/app/[orgId]/settings/(private)/access/approvals/page.tsx @@ -12,6 +12,11 @@ import type { ListRolesResponse } from "@server/routers/role"; import type { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Approvals" +}; export interface ApprovalFeedPageProps { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 69c3da485..2bb88963d 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -7,6 +7,11 @@ import { getTranslations } from "next-intl/server"; import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; import { build } from "@server/build"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Billing" +}; type BillingSettingsProps = { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx index 37334e342..90b89f76f 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx @@ -97,7 +97,8 @@ export default function GeneralPage() { emailPath: z.string().nullable().optional(), namePath: z.string().nullable().optional(), scopes: z.string().min(1, { message: t("idpScopeRequired") }), - autoProvision: z.boolean().default(false) + autoProvision: z.boolean().default(false), + orgMapping: z.string().optional() }); // Google form schema (simplified) @@ -109,7 +110,8 @@ export default function GeneralPage() { .min(1, { message: t("idpClientSecretRequired") }), roleMapping: z.string().nullable().optional(), roleId: z.number().nullable().optional(), - autoProvision: z.boolean().default(false) + autoProvision: z.boolean().default(false), + orgMapping: z.string().optional() }); // Azure form schema (simplified with tenant ID) @@ -122,7 +124,8 @@ export default function GeneralPage() { tenantId: z.string().min(1, { message: t("idpTenantIdRequired") }), roleMapping: z.string().nullable().optional(), roleId: z.number().nullable().optional(), - autoProvision: z.boolean().default(false) + autoProvision: z.boolean().default(false), + orgMapping: z.string().optional() }); type OidcFormValues = z.infer; @@ -160,7 +163,8 @@ export default function GeneralPage() { autoProvision: true, roleMapping: null, roleId: null, - tenantId: "" + tenantId: "", + orgMapping: "" } }); @@ -227,7 +231,8 @@ export default function GeneralPage() { clientSecret: data.idpOidcConfig.clientSecret, autoProvision: data.idp.autoProvision, roleMapping: roleMapping || null, - roleId: null + roleId: null, + orgMapping: data.idpOrg?.orgMapping ?? "" }; // Add variant-specific fields @@ -344,12 +349,14 @@ export default function GeneralPage() { } // Build payload based on variant + const orgMappingTrimmed = data.orgMapping?.trim() ?? ""; let payload: any = { name: data.name, clientId: data.clientId, clientSecret: data.clientSecret, autoProvision: data.autoProvision, - roleMapping: roleMappingExpression + roleMapping: roleMappingExpression, + orgMapping: orgMappingTrimmed === "" ? null : orgMappingTrimmed }; // Add variant-specific fields @@ -532,6 +539,10 @@ export default function GeneralPage() { } rawExpression={rawRoleExpression} onRawExpressionChange={setRawRoleExpression} + orgMappingField={{ + control: form.control, + name: "orgMapping" + }} /> diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx index 6cdbf23c0..2d57d878b 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx @@ -6,6 +6,11 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Identity Provider" +}; interface SettingsLayoutProps { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/page.tsx index ecc2aa835..a9c69d6bb 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Identity Provider" +}; + export default async function IdpPage(props: { params: Promise<{ orgId: string; idpId: string }>; }) { diff --git a/src/app/[orgId]/settings/(private)/idp/create/layout.tsx b/src/app/[orgId]/settings/(private)/idp/create/layout.tsx new file mode 100644 index 000000000..8f606fca1 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/idp/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create Identity Provider" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index 10d86b976..a7796e2a9 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -91,7 +91,8 @@ export default function Page() { tenantId: z.string().optional(), autoProvision: z.boolean().default(false), roleMapping: z.string().nullable().optional(), - roleId: z.number().nullable().optional() + roleId: z.number().nullable().optional(), + orgMapping: z.string().optional() }); type CreateIdpFormValues = z.infer; @@ -112,7 +113,8 @@ export default function Page() { tenantId: "", autoProvision: false, roleMapping: null, - roleId: null + roleId: null, + orgMapping: "" } }); @@ -177,7 +179,7 @@ export default function Page() { return; } - const payload = { + const payload: Record = { name: data.name, clientId: data.clientId, clientSecret: data.clientSecret, @@ -191,6 +193,10 @@ export default function Page() { scopes: data.scopes, variant: data.type }; + const trimmedOrgMapping = data.orgMapping?.trim(); + if (trimmedOrgMapping) { + payload.orgMapping = trimmedOrgMapping; + } // Use the appropriate endpoint based on provider type const endpoint = "oidc"; @@ -336,6 +342,10 @@ export default function Page() { } rawExpression={rawRoleExpression} onRawExpressionChange={setRawRoleExpression} + orgMappingField={{ + control: form.control, + name: "orgMapping" + }} /> diff --git a/src/app/[orgId]/settings/(private)/idp/page.tsx b/src/app/[orgId]/settings/(private)/idp/page.tsx index cd0bc5566..27d636fa5 100644 --- a/src/app/[orgId]/settings/(private)/idp/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/page.tsx @@ -7,6 +7,11 @@ import { getTranslations } from "next-intl/server"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { IdpGlobalModeBanner } from "@app/components/IdpGlobalModeBanner"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Identity Providers" +}; type OrgIdpPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/(private)/license/page.tsx b/src/app/[orgId]/settings/(private)/license/page.tsx index 1ecc94c19..6327689b3 100644 --- a/src/app/[orgId]/settings/(private)/license/page.tsx +++ b/src/app/[orgId]/settings/(private)/license/page.tsx @@ -3,6 +3,11 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types"; import { AxiosResponse } from "axios"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Enterprise Licenses" +}; type Props = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx index 5b9fd628d..a368ec687 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Remote Exit Node" +}; + export default async function RemoteExitNodePage(props: { params: Promise<{ orgId: string; remoteExitNodeId: string }>; }) { diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/layout.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/layout.tsx new file mode 100644 index 000000000..e0c382654 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create Remote Exit Node" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx index 2da0e0da5..2c34d92ec 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx @@ -7,6 +7,11 @@ import ExitNodesTable, { } from "@app/components/ExitNodesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Remote Exit Nodes" +}; type RemoteExitNodesPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index ae37c3752..84a864ba8 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -11,6 +11,11 @@ import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Invitations" +}; type InvitationsPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/access/page.tsx b/src/app/[orgId]/settings/access/page.tsx index 229ffffbc..f6df6ed3a 100644 --- a/src/app/[orgId]/settings/access/page.tsx +++ b/src/app/[orgId]/settings/access/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Access" +}; + type AccessPageProps = { params: Promise<{ orgId: string }>; }; diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index 7165d9e6c..c1ecb2b12 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -8,6 +8,11 @@ import RolesTable, { type RoleRow } from "@app/components/RolesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Roles" +}; type RolesPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx index 7d527f84e..0a9815c36 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx @@ -8,6 +8,11 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { cache } from "react"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "User" +}; interface UserLayoutProps { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/access/users/[userId]/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/page.tsx index 041537286..c56533dad 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "User" +}; + export default async function UserPage(props: { params: Promise<{ orgId: string; userId: string }>; }) { diff --git a/src/app/[orgId]/settings/access/users/create/layout.tsx b/src/app/[orgId]/settings/access/users/create/layout.tsx new file mode 100644 index 000000000..2796ddbc0 --- /dev/null +++ b/src/app/[orgId]/settings/access/users/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create User" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 04d347698..858ac8da8 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -46,7 +46,7 @@ import { Checkbox } from "@app/components/ui/checkbox"; import { ListIdpsResponse } from "@server/routers/idp"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; -import Image from "next/image"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import OrgRolesTagField from "@app/components/OrgRolesTagField"; @@ -152,31 +152,8 @@ export default function Page() { const getIdpIcon = (variant: string | null) => { if (!variant) return null; - - switch (variant.toLowerCase()) { - case "google": - return ( - {t("idpGoogleAlt")} - ); - case "azure": - return ( - {t("idpAzureAlt")} - ); - default: - return null; - } + const type = variant.toLowerCase(); + return ; }; const validFor = [ diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 84685cc04..23c1d69c6 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -11,6 +11,11 @@ import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Users" +}; type UsersPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx index 19b695ca2..300058432 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx @@ -7,6 +7,11 @@ import { GetApiKeyResponse } from "@server/routers/apiKeys"; import ApiKeyProvider from "@app/providers/ApiKeyProvider"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "API Key" +}; interface SettingsLayoutProps { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx index 518db250b..63516208d 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "API Key" +}; + export default async function ApiKeysPage(props: { params: Promise<{ orgId: string; apiKeyId: string }>; }) { diff --git a/src/app/[orgId]/settings/api-keys/create/layout.tsx b/src/app/[orgId]/settings/api-keys/create/layout.tsx new file mode 100644 index 000000000..22e868c85 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create API Key" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx index 0ed9553af..d06e0983b 100644 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -2,11 +2,14 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import OrgApiKeysTable, { - OrgApiKeyRow -} from "@app/components/OrgApiKeysTable"; +import OrgApiKeysTable, { OrgApiKeyRow } from "@app/components/OrgApiKeysTable"; import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "API Keys" +}; type ApiKeyPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx b/src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx index fe3ae4b9f..102b7b781 100644 --- a/src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx +++ b/src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx @@ -17,7 +17,7 @@ type BluePrintsPageProps = { }; export const metadata: Metadata = { - title: "Blueprint Detail" + title: "Edit Blueprint" }; export default async function BluePrintDetailPage(props: BluePrintsPageProps) { diff --git a/src/app/[orgId]/settings/blueprints/create/page.tsx b/src/app/[orgId]/settings/blueprints/create/page.tsx index e7a0490e2..17fe60bf2 100644 --- a/src/app/[orgId]/settings/blueprints/create/page.tsx +++ b/src/app/[orgId]/settings/blueprints/create/page.tsx @@ -12,7 +12,7 @@ export interface CreateBlueprintPageProps { } export const metadata: Metadata = { - title: "Create blueprint" + title: "Create Blueprint" }; export default async function CreateBlueprintPage( diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx index 145fb1728..9d13e6ba4 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx @@ -8,6 +8,11 @@ import { GetClientResponse } from "@server/routers/client"; import { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Machine Client" +}; type SettingsLayoutProps = { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx index 3aa4a2c4a..50594c62e 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Machine Client" +}; + export default async function ClientPage(props: { params: Promise<{ orgId: string; niceId: number | string }>; }) { diff --git a/src/app/[orgId]/settings/clients/machine/create/layout.tsx b/src/app/[orgId]/settings/clients/machine/create/layout.tsx new file mode 100644 index 000000000..945b20a99 --- /dev/null +++ b/src/app/[orgId]/settings/clients/machine/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create Machine Client" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index 4b40c906c..fe9281ac7 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -8,6 +8,11 @@ import { ListClientsResponse } from "@server/routers/client"; import { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; import type { Pagination } from "@server/types/Pagination"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Machine Clients" +}; type ClientsPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index aeea1c83f..dcb8a2b84 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Clients" +}; + type ClientsPageProps = { params: Promise<{ orgId: string }>; searchParams: Promise<{ view?: string }>; diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx index 2d9934cbe..9d3b169d8 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx @@ -8,6 +8,11 @@ import { GetClientResponse } from "@server/routers/client"; import { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "User Device" +}; type SettingsLayoutProps = { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/page.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/page.tsx index 9ad97186d..a2c798c1c 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/page.tsx @@ -1,10 +1,13 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "User Device" +}; + export default async function ClientPage(props: { params: Promise<{ orgId: string; niceId: number | string }>; }) { const params = await props.params; - redirect( - `/${params.orgId}/settings/clients/user/${params.niceId}/general` - ); + redirect(`/${params.orgId}/settings/clients/user/${params.niceId}/general`); } diff --git a/src/app/[orgId]/settings/clients/user/page.tsx b/src/app/[orgId]/settings/clients/user/page.tsx index fcb24e4e3..23fba583a 100644 --- a/src/app/[orgId]/settings/clients/user/page.tsx +++ b/src/app/[orgId]/settings/clients/user/page.tsx @@ -7,6 +7,11 @@ import { type ListUserDevicesResponse } from "@server/routers/client"; import type { Pagination } from "@server/types/Pagination"; import { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "User Devices" +}; type ClientsPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index 23a79737d..9f9878967 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -1,16 +1,13 @@ -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import DomainInfoCard from "@app/components/DomainInfoCard"; -import RestartDomainButton from "@app/components/RestartDomainButton"; +import DomainPageClient from "@app/components/DomainPageClient"; import { GetDomainResponse } from "@server/routers/domain/getDomain"; -import { pullEnv } from "@app/lib/pullEnv"; -import { getTranslations } from "next-intl/server"; -import RefreshButton from "@app/components/RefreshButton"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { GetDNSRecordsResponse } from "@server/routers/domain"; -import DNSRecordsTable from "@app/components/DNSRecordTable"; -import DomainCertForm from "@app/components/DomainCertForm"; -import { build } from "@server/build"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Domain" +}; interface DomainSettingsPageProps { params: Promise<{ domainId: string; orgId: string }>; @@ -20,8 +17,6 @@ export default async function DomainSettingsPage({ params }: DomainSettingsPageProps) { const { domainId, orgId } = await params; - const t = await getTranslations(); - const env = pullEnv(); let domain: GetDomainResponse | null = null; try { @@ -34,57 +29,27 @@ export default async function DomainSettingsPage({ return null; } - let dnsRecords; + let dnsRecords: GetDNSRecordsResponse | null = null; try { const response = await internal.get( `/org/${orgId}/domain/${domainId}/dns-records`, await authCookieHeader() ); dnsRecords = response.data.data; - } catch (error) { + } catch { return null; } - if (!domain) { + if (!domain || !dnsRecords) { return null; } return ( - <> -
- - {env.flags.usePangolinDns && domain.failed ? ( - - ) : ( - - )} -
-
- {build != "oss" && env.flags.usePangolinDns ? ( - - ) : null} - - - - {domain.type == "wildcard" && !domain.configManaged && ( - - )} -
- + ); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx index d1325d32b..affad2551 100644 --- a/src/app/[orgId]/settings/domains/page.tsx +++ b/src/app/[orgId]/settings/domains/page.tsx @@ -11,6 +11,11 @@ import OrgProvider from "@app/providers/OrgProvider"; import { ListDomainsResponse } from "@server/routers/domain"; import { toUnicode } from "punycode"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Domains" +}; type Props = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx index 0bd482864..7712334f1 100644 --- a/src/app/[orgId]/settings/general/auth-page/page.tsx +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -11,6 +11,7 @@ import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import { AxiosResponse } from "axios"; +import type { Metadata } from "next"; import { redirect } from "next/navigation"; export interface AuthPageProps { diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 736e2037e..8620cd529 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -11,6 +11,11 @@ import { getCachedOrg } from "@app/lib/api/getCachedOrg"; import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; import { build } from "@server/build"; import { pullEnv } from "@app/lib/pullEnv"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Organization" +}; type GeneralSettingsProps = { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/logs/access/layout.tsx b/src/app/[orgId]/settings/logs/access/layout.tsx new file mode 100644 index 000000000..07d7f6f28 --- /dev/null +++ b/src/app/[orgId]/settings/logs/access/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Access Logs" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/logs/action/layout.tsx b/src/app/[orgId]/settings/logs/action/layout.tsx new file mode 100644 index 000000000..889617712 --- /dev/null +++ b/src/app/[orgId]/settings/logs/action/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Action Logs" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/logs/analytics/page.tsx b/src/app/[orgId]/settings/logs/analytics/page.tsx index f5bd4e7aa..9246c3cbb 100644 --- a/src/app/[orgId]/settings/logs/analytics/page.tsx +++ b/src/app/[orgId]/settings/logs/analytics/page.tsx @@ -2,6 +2,11 @@ import { LogAnalyticsData } from "@app/components/LogAnalyticsData"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; import { Suspense } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Log Analytics" +}; export interface AnalyticsPageProps { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/logs/connection/layout.tsx b/src/app/[orgId]/settings/logs/connection/layout.tsx new file mode 100644 index 000000000..20d93f802 --- /dev/null +++ b/src/app/[orgId]/settings/logs/connection/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Connection Logs" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/logs/page.tsx b/src/app/[orgId]/settings/logs/page.tsx index d9663e721..7c2a6532b 100644 --- a/src/app/[orgId]/settings/logs/page.tsx +++ b/src/app/[orgId]/settings/logs/page.tsx @@ -1,3 +1,9 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Logs" +}; + export default function GeneralPage() { return null; } diff --git a/src/app/[orgId]/settings/logs/request/layout.tsx b/src/app/[orgId]/settings/logs/request/layout.tsx new file mode 100644 index 000000000..61c3a3a7d --- /dev/null +++ b/src/app/[orgId]/settings/logs/request/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Request Logs" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/logs/streaming/layout.tsx b/src/app/[orgId]/settings/logs/streaming/layout.tsx new file mode 100644 index 000000000..a5baea411 --- /dev/null +++ b/src/app/[orgId]/settings/logs/streaming/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Streaming Logs" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/page.tsx b/src/app/[orgId]/settings/page.tsx index 9956bc859..bf8beab72 100644 --- a/src/app/[orgId]/settings/page.tsx +++ b/src/app/[orgId]/settings/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Settings" +}; + type OrgPageProps = { params: Promise<{ orgId: string }>; }; diff --git a/src/app/[orgId]/settings/provisioning/keys/page.tsx b/src/app/[orgId]/settings/provisioning/keys/page.tsx index 32a06706d..fc95a655d 100644 --- a/src/app/[orgId]/settings/provisioning/keys/page.tsx +++ b/src/app/[orgId]/settings/provisioning/keys/page.tsx @@ -12,6 +12,11 @@ import DismissableBanner from "@app/components/DismissableBanner"; import Link from "next/link"; import { Button } from "@app/components/ui/button"; import { ArrowRight, Plug } from "lucide-react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Provisioning Keys" +}; type ProvisioningKeysPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/provisioning/page.tsx b/src/app/[orgId]/settings/provisioning/page.tsx index 51db66c2d..1e0377590 100644 --- a/src/app/[orgId]/settings/provisioning/page.tsx +++ b/src/app/[orgId]/settings/provisioning/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Provisioning" +}; + type ProvisioningPageProps = { params: Promise<{ orgId: string }>; }; @@ -7,4 +12,4 @@ type ProvisioningPageProps = { export default async function ProvisioningPage(props: ProvisioningPageProps) { const params = await props.params; redirect(`/${params.orgId}/settings/provisioning/keys`); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/provisioning/pending/page.tsx b/src/app/[orgId]/settings/provisioning/pending/page.tsx index 4669f9160..ee7246821 100644 --- a/src/app/[orgId]/settings/provisioning/pending/page.tsx +++ b/src/app/[orgId]/settings/provisioning/pending/page.tsx @@ -11,6 +11,11 @@ import { Button } from "@app/components/ui/button"; import { ArrowRight, Plug } from "lucide-react"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Pending Sites" +}; type PendingSitesPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index c15e3d429..da967feea 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -10,8 +10,13 @@ import type { ListResourcesResponse } from "@server/routers/resource"; import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; import type { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Private Resources" +}; + export interface ClientResourcesPageProps { params: Promise<{ orgId: string }>; searchParams: Promise>; diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index 954b966ac..55ebe6554 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Public Resources" +}; + export interface ResourcesPageProps { params: Promise<{ orgId: string }>; } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index f410b4c8b..2f6cd1492 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -14,6 +14,11 @@ import OrgProvider from "@app/providers/OrgProvider"; import { cache } from "react"; import ResourceInfoBox from "@app/components/ResourceInfoBox"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Public Resource" +}; export const dynamic = "force-dynamic"; diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx index 5ec1cf00d..06a4af045 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Public Resource" +}; + export default async function ResourcePage(props: { params: Promise<{ niceId: string; orgId: string }>; }) { diff --git a/src/app/[orgId]/settings/resources/proxy/create/layout.tsx b/src/app/[orgId]/settings/resources/proxy/create/layout.tsx new file mode 100644 index 000000000..7e635a730 --- /dev/null +++ b/src/app/[orgId]/settings/resources/proxy/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create Public Resource" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/resources/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/page.tsx index 05425b4bd..cdbf959f4 100644 --- a/src/app/[orgId]/settings/resources/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/page.tsx @@ -13,6 +13,11 @@ import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { toUnicode } from "punycode"; import { cache } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Public Resources" +}; export interface ProxyResourcesPageProps { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index b41a3d1ce..1a732f714 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -7,10 +7,13 @@ import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; -import ShareLinksTable, { - ShareLinkRow -} from "@app/components/ShareLinksTable"; +import ShareLinksTable, { ShareLinkRow } from "@app/components/ShareLinksTable"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Shareable Links" +}; type ShareLinksPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index aa02bb667..d5e11e9bc 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -8,7 +8,11 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SiteInfoCard from "@app/components/SiteInfoCard"; import { getTranslations } from "next-intl/server"; +import type { Metadata } from "next"; +export const metadata: Metadata = { + title: "Site" +}; interface SettingsLayoutProps { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/sites/[niceId]/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/page.tsx index 045b762e3..8f505e85c 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Site" +}; + export default async function SitePage(props: { params: Promise<{ orgId: string; niceId: string }>; }) { diff --git a/src/app/[orgId]/settings/sites/create/layout.tsx b/src/app/[orgId]/settings/sites/create/layout.tsx new file mode 100644 index 000000000..fc8f1edf2 --- /dev/null +++ b/src/app/[orgId]/settings/sites/create/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Create Site" +}; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 38083325b..d78666d78 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -5,8 +5,13 @@ import { AxiosResponse } from "axios"; import SitesTable, { SiteRow } from "@app/components/SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SitesBanner from "@app/components/SitesBanner"; +import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +export const metadata: Metadata = { + title: "Sites" +}; + type SitesPageProps = { params: Promise<{ orgId: string }>; searchParams: Promise>; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index 60e8a094a..e9438da33 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -20,7 +20,6 @@ import { import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, @@ -63,7 +62,7 @@ import { SettingsSectionForm } from "@app/components/Settings"; import { useTranslations } from "next-intl"; -import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields"; +import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget"; import { compileRoleMappingExpression, createMappingBuilderRule, @@ -499,9 +498,17 @@ export default function PoliciesPage() { id="policy-default-mappings-form" className="space-y-6" > - {}} + orgMappingField={{ + control: defaultMappingsForm.control, + name: "defaultOrgMapping", + labelKey: "defaultMappingsOrg" + }} + roleMappingFieldIdPrefix="admin-idp-default-role" + showFreeformRoleNamesHint roleMappingMode={defaultRoleMappingMode} onRoleMappingModeChange={ setDefaultRoleMappingMode @@ -528,27 +535,6 @@ export default function PoliciesPage() { setDefaultRawRoleExpression } /> - - ( - - - {t("defaultMappingsOrg")} - - - - - - {t( - "defaultMappingsOrgDescription" - )} - - - - )} - /> @@ -687,9 +673,15 @@ export default function PoliciesPage() { )} /> - {}} + orgMappingField={{ + control: form.control, + name: "orgMapping" + }} + roleMappingFieldIdPrefix="admin-idp-policy-role" roleMappingMode={policyRoleMappingMode} onRoleMappingModeChange={ setPolicyRoleMappingMode @@ -716,27 +708,6 @@ export default function PoliciesPage() { setPolicyRawRoleExpression } /> - - ( - - - {t("orgMappingPathOptional")} - - - - - - {t( - "defaultMappingsOrgDescription" - )} - - - - )} - /> diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 82036c510..6e3270a55 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -24,7 +24,6 @@ import { import HeaderTitle from "@app/components/SettingsSectionTitle"; import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -34,7 +33,6 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { applyOidcIdpProviderType } from "@app/lib/idp/oidcIdpProviderDefaults"; import { zodResolver } from "@hookform/resolvers/zod"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { InfoIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -220,23 +218,6 @@ export default function Page() { )} /> -
- { - form.setValue( - "autoProvision", - checked - ); - }} - /> -
@@ -244,6 +225,32 @@ export default function Page() { + + + + {t("idpAutoProvisionUsers")} + + + + + + +
+ { + form.setValue("autoProvision", checked); + }} + /> +

+ {t("idpAutoProvisionConfigureAfterCreate")} +

+
+
+
+
- - - + + + )} diff --git a/src/components/AutoProvisionConfigWidget.tsx b/src/components/AutoProvisionConfigWidget.tsx index d4df3f50d..4767544d0 100644 --- a/src/components/AutoProvisionConfigWidget.tsx +++ b/src/components/AutoProvisionConfigWidget.tsx @@ -1,19 +1,33 @@ "use client"; -import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription"; -import { FormDescription } from "@app/components/ui/form"; +import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields"; import { SwitchInput } from "@app/components/SwitchInput"; -import { useTranslations } from "next-intl"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { MappingBuilderRule, RoleMappingMode } from "@app/lib/idpRoleMapping"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { MappingBuilderRule, RoleMappingMode } from "@app/lib/idpRoleMapping"; -import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields"; +import { useTranslations } from "next-intl"; +import type { Control } from "react-hook-form"; type Role = { roleId: number; name: string; }; +export type IdpOrgMappingFieldBinding = { + control: unknown; + name: string; + labelKey?: string; +}; + type AutoProvisionConfigWidgetProps = { autoProvision: boolean; onAutoProvisionChange: (checked: boolean) => void; @@ -28,6 +42,11 @@ type AutoProvisionConfigWidgetProps = { onMappingBuilderRulesChange: (rules: MappingBuilderRule[]) => void; rawExpression: string; onRawExpressionChange: (expression: string) => void; + orgMappingField: IdpOrgMappingFieldBinding; + showAutoProvisionSwitch?: boolean; + roleMappingFieldIdPrefix?: string; + showFreeformRoleNamesHint?: boolean; + autoProvisionSwitchId?: string; }; export default function AutoProvisionConfigWidget({ @@ -43,41 +62,95 @@ export default function AutoProvisionConfigWidget({ mappingBuilderRules, onMappingBuilderRulesChange, rawExpression, - onRawExpressionChange + onRawExpressionChange, + orgMappingField, + showAutoProvisionSwitch = true, + roleMappingFieldIdPrefix = "org-idp-auto-provision", + showFreeformRoleNamesHint = false, + autoProvisionSwitchId = "auto-provision-toggle" }: AutoProvisionConfigWidgetProps) { const t = useTranslations(); const { isPaidUser } = usePaidStatus(); + const showMappingTabs = showAutoProvisionSwitch === false || autoProvision; + + const orgMappingLabelKey = + orgMappingField.labelKey ?? "orgMappingPathOptional"; + return (
-
- -
+ {showAutoProvisionSwitch && ( +
+ +
+ )} - {autoProvision && ( - + {showMappingTabs && ( + +
+ +
+
+
+

+ {t("defaultMappingsOrgDescription")} +

+ + } + name={orgMappingField.name} + render={({ field }) => ( + + + {t(orgMappingLabelKey)} + + + + + + + )} + /> +
+
+
)}
); diff --git a/src/components/DomainPageClient.tsx b/src/components/DomainPageClient.tsx new file mode 100644 index 000000000..31527c5b8 --- /dev/null +++ b/src/components/DomainPageClient.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { domainQueries } from "@app/lib/queries"; +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import { GetDNSRecordsResponse } from "@server/routers/domain"; +import DomainInfoCard from "@app/components/DomainInfoCard"; +import DNSRecordsTable from "@app/components/DNSRecordTable"; +import RestartDomainButton from "@app/components/RestartDomainButton"; +import RefreshButton from "@app/components/RefreshButton"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import DomainCertForm from "@app/components/DomainCertForm"; +import { build } from "@server/build"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; + +interface DomainPageClientProps { + initialDomain: GetDomainResponse; + initialDnsRecords: GetDNSRecordsResponse; + orgId: string; + domainId: string; +} + +export default function DomainPageClient({ + initialDomain, + initialDnsRecords, + orgId, + domainId +}: DomainPageClientProps) { + const t = useTranslations(); + const { env } = useEnvContext(); + + const { data: domain, refetch: refetchDomain } = useQuery({ + ...domainQueries.getDomain({ orgId, domainId }), + initialData: initialDomain + }); + + const { data: dnsRecords, refetch: refetchDnsRecords } = useQuery({ + ...domainQueries.getDNSRecords({ orgId, domainId }), + initialData: initialDnsRecords + }); + + const refetchAll = () => { + refetchDomain(); + refetchDnsRecords(); + }; + + return ( + <> +
+ + {env.flags.usePangolinDns && domain.failed ? ( + + ) : ( + + )} +
+
+ {build !== "oss" && env.flags.usePangolinDns ? ( + + ) : null} + + ({ + ...r, + id: String(r.id) + }))} + type={domain.type} + /> + + {domain.type === "wildcard" && !domain.configManaged && ( + + )} +
+ + ); +} \ No newline at end of file diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index f5cb1ae74..2c3abeb1a 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -10,13 +10,12 @@ import { MoreHorizontal, RefreshCw } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Badge } from "@app/components/ui/badge"; -import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; @@ -34,6 +33,10 @@ import { TooltipTrigger } from "./ui/tooltip"; import Link from "next/link"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { orgQueries } from "@app/lib/queries"; +import { toUnicode } from "punycode"; +import { durationToMs } from "@app/lib/durationToMs"; export type DomainRow = { domainId: string; @@ -59,32 +62,32 @@ export default function DomainsTable({ domains, orgId }: Props) { const [selectedDomain, setSelectedDomain] = useState( null ); - const [isRefreshing, setIsRefreshing] = useState(false); const [restartingDomains, setRestartingDomains] = useState>( new Set() ); const env = useEnvContext(); const api = createApiClient(env); - const router = useRouter(); const t = useTranslations(); const { toast } = useToast(); const { org } = useOrgContext(); + const queryClient = useQueryClient(); - const refreshData = async () => { - setIsRefreshing(true); - try { - await new Promise((resolve) => setTimeout(resolve, 200)); - router.refresh(); - } catch (error) { - toast({ - title: t("error"), - description: t("refreshError"), - variant: "destructive" - }); - } finally { - setIsRefreshing(false); - } - }; + const { data: rawDomains, isRefetching, refetch } = useQuery({ + ...orgQueries.domains({ orgId }), + initialData: domains as any, + refetchInterval: durationToMs(10, "seconds") + }); + + const tableData = useMemo( + () => + (rawDomains ?? []).map((d) => ({ + ...d, + baseDomain: toUnicode(d.baseDomain), + type: d.type ?? "", + errorMessage: d.errorMessage ?? null + } as DomainRow)), + [rawDomains] + ); const deleteDomain = async (domainId: string) => { try { @@ -94,7 +97,7 @@ export default function DomainsTable({ domains, orgId }: Props) { description: t("domainDeletedDescription") }); setIsDeleteModalOpen(false); - refreshData(); + refetch(); } catch (e) { toast({ title: t("error"), @@ -114,7 +117,7 @@ export default function DomainsTable({ domains, orgId }: Props) { fallback: "Domain verification restarted successfully" }) }); - refreshData(); + refetch(); } catch (e) { toast({ title: t("error"), @@ -361,16 +364,16 @@ export default function DomainsTable({ domains, orgId }: Props) { open={isCreateModalOpen} setOpen={setIsCreateModalOpen} onCreated={(domain) => { - refreshData(); + refetch(); }} /> setIsCreateModalOpen(true)} - onRefresh={refreshData} - isRefreshing={isRefreshing} + onRefresh={refetch} + isRefreshing={isRefetching} /> ); diff --git a/src/components/IdpLoginButtons.tsx b/src/components/IdpLoginButtons.tsx index 50d849812..4fc4c9901 100644 --- a/src/components/IdpLoginButtons.tsx +++ b/src/components/IdpLoginButtons.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { Button } from "@app/components/ui/button"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { useTranslations } from "next-intl"; -import Image from "next/image"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; import { generateOidcUrlProxy, type GenerateOidcUrlResponse @@ -135,24 +135,7 @@ export default function IdpLoginButtons({ disabled={loading} loading={loading} > - {effectiveType === "google" && ( - Google - )} - {effectiveType === "azure" && ( - Azure - )} + {idp.name} ); diff --git a/src/components/IdpTypeBadge.tsx b/src/components/IdpTypeBadge.tsx index b0e90660b..d18c96d9b 100644 --- a/src/components/IdpTypeBadge.tsx +++ b/src/components/IdpTypeBadge.tsx @@ -1,7 +1,7 @@ "use client"; import { Badge } from "@app/components/ui/badge"; -import Image from "next/image"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; type IdpTypeBadgeProps = { type: string; @@ -29,34 +29,8 @@ export default function IdpTypeBadge({ variant="secondary" className="inline-flex items-center space-x-1 w-fit" > - {effectiveType === "google" && ( - <> - Google - {effectiveName} - - )} - {effectiveType === "azure" && ( - <> - Azure - {effectiveName} - - )} - {effectiveType === "oidc" && {effectiveName}} - {!["google", "azure", "oidc"].includes(effectiveType) && ( - {effectiveName} - )} + + {effectiveName} ); } diff --git a/src/components/IdpTypeIcon.tsx b/src/components/IdpTypeIcon.tsx new file mode 100644 index 000000000..be49f9654 --- /dev/null +++ b/src/components/IdpTypeIcon.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { cn } from "@app/lib/cn"; +import Image from "next/image"; +import { ReactNode } from "react"; + +type Props = { + type?: string | null; + variant?: string | null; + size?: number; + className?: string; + alt?: string; + fallback?: ReactNode; +}; + +export default function IdpTypeIcon({ + type, + variant, + size = 16, + className, + alt, + fallback = null +}: Props) { + const effectiveType = (variant || type || "").toLowerCase(); + + let src: string | null = null; + let defaultAlt = ""; + + if (effectiveType === "google") { + src = "/idp/google.png"; + defaultAlt = "Google"; + } else if (effectiveType === "azure") { + src = "/idp/azure.png"; + defaultAlt = "Azure"; + } else if (effectiveType === "oidc") { + src = "/idp/openid.png"; + defaultAlt = "OAuth2/OIDC"; + } + + if (!src) { + return <>{fallback}; + } + + return ( + {alt + ); +} diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index 779d5eb74..27d8c2cd8 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -221,11 +221,11 @@ export function LayoutSidebar({ )}
- {canShowProductUpdates && ( + {canShowProductUpdates ? (
- )} + ) :
} {build === "enterprise" && (
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index c3b1fc384..e87a8b1a8 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -27,7 +27,6 @@ import { LockIcon } from "lucide-react"; import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton"; import { createApiClient } from "@app/lib/api"; import Link from "next/link"; -import Image from "next/image"; import { GenerateOidcUrlResponse } from "@server/routers/idp"; import { Separator } from "./ui/separator"; import { useTranslations } from "next-intl"; @@ -37,6 +36,7 @@ import { } from "@app/actions/server"; import { redirect as redirectTo } from "next/navigation"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; // @ts-ignore import { loadReoScript } from "reodotdev"; import { build } from "@server/build"; @@ -393,24 +393,7 @@ export default function LoginForm({ loginWithIdp(idp.idpId); }} > - {effectiveType === "google" && ( - Google - )} - {effectiveType === "azure" && ( - Azure - )} + {idp.name} ); diff --git a/src/components/OrgIdpDataTable.tsx b/src/components/OrgIdpDataTable.tsx index 9a45f49e8..7e3f7ab65 100644 --- a/src/components/OrgIdpDataTable.tsx +++ b/src/components/OrgIdpDataTable.tsx @@ -1,19 +1,24 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; +import { + DataTable, + type DataTableAddAction +} from "@app/components/ui/data-table"; import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; onAdd?: () => void; + addActions?: DataTableAddAction[]; } export function IdpDataTable({ columns, data, - onAdd + onAdd, + addActions }: DataTableProps) { const t = useTranslations(); @@ -27,6 +32,7 @@ export function IdpDataTable({ searchColumn="name" addButtonText={t("idpAdd")} onAdd={onAdd} + addActions={addActions} enableColumnVisibility={true} stickyRightColumn="actions" /> diff --git a/src/components/OrgIdpTable.tsx b/src/components/OrgIdpTable.tsx index 8f53f4847..0e3a83dc2 100644 --- a/src/components/OrgIdpTable.tsx +++ b/src/components/OrgIdpTable.tsx @@ -4,13 +4,37 @@ import { ColumnDef } from "@tanstack/react-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { IdpDataTable } from "@app/components/OrgIdpDataTable"; import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; -import { useState } from "react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { + ArrowRight, + ArrowUpDown, + KeyRound, + MoreHorizontal +} from "lucide-react"; +import { useMemo, useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useUserContext } from "@app/hooks/useUserContext"; import { useRouter } from "next/navigation"; import { DropdownMenu, @@ -21,6 +45,13 @@ import { import Link from "next/link"; import { useTranslations } from "next-intl"; import IdpTypeBadge from "@app/components/IdpTypeBadge"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; +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 { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; export type IdpRow = { idpId: number; @@ -29,6 +60,15 @@ export type IdpRow = { variant?: string; }; +type AdminIdpRow = ListUserAdminOrgIdpsResponse["idps"][number]; + +function IdpImportRowIcon({ + type, + variant +}: Pick) { + return ; +} + type Props = { idps: IdpRow[]; orgId: string; @@ -37,10 +77,51 @@ type Props = { export default function IdpTable({ idps, orgId }: Props) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedIdp, setSelectedIdp] = useState(null); + const [isUnassociateModalOpen, setIsUnassociateModalOpen] = useState(false); + const [selectedUnassociateIdp, setSelectedUnassociateIdp] = + useState(null); + const [importDialogOpen, setImportDialogOpen] = useState(false); + const [importSearchQuery, setImportSearchQuery] = useState(""); + const [importSubmitting, setImportSubmitting] = useState(false); + const [debouncedImportSearch] = useDebounce(importSearchQuery, 150); + const api = createApiClient(useEnvContext()); + const { user } = useUserContext(); + const { isPaidUser } = usePaidStatus(); const router = useRouter(); const t = useTranslations(); + const canImportOrgOidcIdp = isPaidUser(tierMatrix.orgOidc); + + const { data: adminIdpsRaw = [] } = useQuery({ + queryKey: ["admin-org-idps", user.userId], + queryFn: async () => { + const res = await api.get<{ + data: ListUserAdminOrgIdpsResponse; + }>(`/user/${user.userId}/admin-org-idps`); + return res.data.data.idps; + }, + enabled: importDialogOpen && !!user?.userId + }); + + const importableIdps = useMemo(() => { + const localIds = new Set(idps.map((i) => i.idpId)); + return adminIdpsRaw.filter( + (row) => row.orgId !== orgId && !localIds.has(row.idpId) + ); + }, [adminIdpsRaw, orgId, idps]); + + const shownImportIdps = useMemo(() => { + const q = debouncedImportSearch.trim().toLowerCase(); + if (!q) { + return importableIdps; + } + return importableIdps.filter((row) => { + const hay = `${row.orgName} ${row.name}`.toLowerCase(); + return hay.includes(q); + }); + }, [importableIdps, debouncedImportSearch]); + const deleteIdp = async (idpId: number) => { try { await api.delete(`/org/${orgId}/idp/${idpId}`); @@ -59,6 +140,49 @@ export default function IdpTable({ idps, orgId }: Props) { } }; + const importIdp = async (row: AdminIdpRow) => { + setImportSubmitting(true); + try { + await api.post(`/org/${orgId}/idp/${row.idpId}/import`, { + sourceOrgId: row.orgId + }); + toast({ + title: t("success"), + description: t("idpImportedDescription") + }); + setImportDialogOpen(false); + setImportSearchQuery(""); + router.refresh(); + router.push(`/${orgId}/settings/idp/${row.idpId}/general`); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e), + variant: "destructive" + }); + } finally { + setImportSubmitting(false); + } + }; + + const unassociateIdp = async (idpId: number) => { + try { + await api.delete(`/org/${orgId}/idp/${idpId}/association`); + toast({ + title: t("success"), + description: t("idpUnassociatedDescription") + }); + setIsUnassociateModalOpen(false); + router.refresh(); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e), + variant: "destructive" + }); + } + }; + const columns: ExtendedColumnDef[] = [ { accessorKey: "idpId", @@ -142,6 +266,14 @@ export default function IdpTable({ idps, orgId }: Props) { {t("viewSettings")} + { + setSelectedUnassociateIdp(siteRow); + setIsUnassociateModalOpen(true); + }} + > + {t("idpUnassociateMenu")} + { setSelectedIdp(siteRow); @@ -149,7 +281,7 @@ export default function IdpTable({ idps, orgId }: Props) { }} > - {t("delete")} + {t("idpDeleteAllOrgsMenu")} @@ -179,8 +311,8 @@ export default function IdpTable({ idps, orgId }: Props) { }} dialog={
-

{t("idpQuestionRemove")}

-

{t("idpMessageRemove")}

+

{t("idpDeleteGlobalQuestion")}

+

{t("idpDeleteGlobalDescription")}

} buttonText={t("idpConfirmDelete")} @@ -189,11 +321,126 @@ export default function IdpTable({ idps, orgId }: Props) { title={t("idpDelete")} /> )} + {selectedUnassociateIdp && ( + { + setIsUnassociateModalOpen(val); + setSelectedUnassociateIdp(null); + }} + dialog={ +
+

{t("idpUnassociateQuestion")}

+

{t("idpUnassociateDescription")}

+
+ } + buttonText={t("idpUnassociateConfirm")} + onConfirm={async () => + unassociateIdp(selectedUnassociateIdp.idpId) + } + string={selectedUnassociateIdp.name} + title={t("idpUnassociateTitle")} + warningText={t("idpUnassociateWarning")} + /> + )} + + { + setImportDialogOpen(open); + if (!open) { + setImportSearchQuery(""); + } + }} + > + + + + {t("idpImportDialogTitle")} + + + {t("idpImportDialogDescription")} + + + + + + + + {t("idpImportEmpty")} + + + {shownImportIdps.map((row) => ( + { + if (!canImportOrgOidcIdp) { + return; + } + void importIdp(row); + }} + > +
+ +
+
+
+ {row.orgName} +
+
+ {row.name} +
+
+
+ ))} +
+
+
+
+ + + + + +
+
router.push(`/${orgId}/settings/idp/create`)} + addActions={[ + { + label: t("idpAddActionCreateNew"), + onSelect: () => { + router.push(`/${orgId}/settings/idp/create`); + } + }, + { + label: t("idpAddActionImportFromOrg"), + onSelect: () => { + setImportDialogOpen(true); + } + } + ]} /> ); diff --git a/src/components/PaidFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx index 95179ea78..933a9f5b9 100644 --- a/src/components/PaidFeaturesAlert.tsx +++ b/src/components/PaidFeaturesAlert.tsx @@ -10,7 +10,8 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { Tier } from "@server/types/Tiers"; import { useParams } from "next/navigation"; -const TIER_ORDER: Tier[] = ["tier1", "tier2", "tier3", "enterprise"]; +// const TIER_ORDER: Tier[] = ["tier1", "tier2", "tier3", "enterprise"]; +const TIER_ORDER: Tier[] = ["tier2", "tier3", "enterprise"]; const TIER_TRANSLATION_KEYS: Record< Tier, diff --git a/src/components/RefreshButton.tsx b/src/components/RefreshButton.tsx index 3ba7d4f32..67799546f 100644 --- a/src/components/RefreshButton.tsx +++ b/src/components/RefreshButton.tsx @@ -7,7 +7,11 @@ import { Button } from "@app/components/ui/button"; import { useTranslations } from "next-intl"; import { toast } from "@app/hooks/useToast"; -export default function RefreshButton() { +interface RefreshButtonProps { + onRefresh?: () => void; +} + +export default function RefreshButton({ onRefresh }: RefreshButtonProps = {}) { const router = useRouter(); const [isRefreshing, setIsRefreshing] = useState(false); const t = useTranslations(); @@ -16,7 +20,11 @@ export default function RefreshButton() { setIsRefreshing(true); try { await new Promise((resolve) => setTimeout(resolve, 200)); - router.refresh(); + if (onRefresh) { + onRefresh(); + } else { + router.refresh(); + } } catch { toast({ title: t("error"), diff --git a/src/components/RestartDomainButton.tsx b/src/components/RestartDomainButton.tsx index 670f4fa2c..5501ad1ed 100644 --- a/src/components/RestartDomainButton.tsx +++ b/src/components/RestartDomainButton.tsx @@ -12,11 +12,13 @@ import { useTranslations } from "next-intl"; interface RestartDomainButtonProps { orgId: string; domainId: string; + onSuccess?: () => void; } export default function RestartDomainButton({ orgId, - domainId + domainId, + onSuccess }: RestartDomainButtonProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); @@ -35,7 +37,11 @@ export default function RestartDomainButton({ }); // Wait a bit before refreshing to allow the restart to take effect await new Promise((resolve) => setTimeout(resolve, 200)); - router.refresh(); + if (onSuccess) { + onSuccess(); + } else { + router.refresh(); + } } catch (e) { toast({ title: t("error"), diff --git a/src/components/RoleMappingConfigFields.tsx b/src/components/RoleMappingConfigFields.tsx index 12790d4aa..d62b7f9e8 100644 --- a/src/components/RoleMappingConfigFields.tsx +++ b/src/components/RoleMappingConfigFields.tsx @@ -79,10 +79,7 @@ export default function RoleMappingConfigFields({ ); useEffect(() => { - if ( - !supportsMultipleRolesPerUser && - mappingBuilderRules.length > 1 - ) { + if (!supportsMultipleRolesPerUser && mappingBuilderRules.length > 1) { onMappingBuilderRulesChange([mappingBuilderRules[0]]); } }, [ @@ -95,11 +92,7 @@ export default function RoleMappingConfigFields({ if (!supportsMultipleRolesPerUser && fixedRoleNames.length > 1) { onFixedRoleNamesChange([fixedRoleNames[0]]); } - }, [ - supportsMultipleRolesPerUser, - fixedRoleNames, - onFixedRoleNamesChange - ]); + }, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]); const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`; const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`; @@ -116,7 +109,6 @@ export default function RoleMappingConfigFields({ return (
- {t("roleMapping")} {t("roleMappingDescription")} @@ -272,7 +264,9 @@ export default function RoleMappingConfigFields({ supportsMultipleRolesPerUser={ supportsMultipleRolesPerUser } - showRemoveButton={mappingBuilderShowsRemoveColumn} + showRemoveButton={ + mappingBuilderShowsRemoveColumn + } rule={rule} onChange={(nextRule) => { const nextRules = mappingBuilderRules.map( @@ -390,12 +384,10 @@ function BuilderRuleRow({ text: name }))} setTags={(nextTags) => { - const prevRoleTags = rule.roleNames.map( - (name) => ({ - id: name, - text: name - }) - ); + const prevRoleTags = rule.roleNames.map((name) => ({ + id: name, + text: name + })); const next = typeof nextTags === "function" ? nextTags(prevRoleTags) diff --git a/src/components/SiteInfoCard.tsx b/src/components/SiteInfoCard.tsx index 2b40d379b..56492ff54 100644 --- a/src/components/SiteInfoCard.tsx +++ b/src/components/SiteInfoCard.tsx @@ -33,7 +33,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { return ( - + {t("identifier")} {site.niceId} @@ -68,6 +68,18 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { {getConnectionTypeString(site.type)} + {site.endpoint && ( + + + {t("publicIpEndpoint")} + + + {site.endpoint.includes(":") + ? site.endpoint.substring(0, site.endpoint.lastIndexOf(":")) + : site.endpoint} + + + )} diff --git a/src/components/idp/OidcIdpProviderTypeSelect.tsx b/src/components/idp/OidcIdpProviderTypeSelect.tsx index 4665d9c0d..038254ebe 100644 --- a/src/components/idp/OidcIdpProviderTypeSelect.tsx +++ b/src/components/idp/OidcIdpProviderTypeSelect.tsx @@ -6,8 +6,8 @@ import { } from "@app/components/StrategySelect"; import { useEnvContext } from "@app/hooks/useEnvContext"; import type { IdpOidcProviderType } from "@app/lib/idp/oidcIdpProviderDefaults"; +import IdpTypeIcon from "@app/components/IdpTypeIcon"; import { useTranslations } from "next-intl"; -import Image from "next/image"; import { useEffect, useMemo } from "react"; type Props = { @@ -32,7 +32,8 @@ export function OidcIdpProviderTypeSelect({ value, onTypeChange }: Props) { { id: "oidc", title: "OAuth2/OIDC", - description: t("idpOidcDescription") + description: t("idpOidcDescription"), + icon: } ]; if (hideTemplates) { @@ -44,29 +45,13 @@ export function OidcIdpProviderTypeSelect({ value, onTypeChange }: Props) { id: "google", title: t("idpGoogleTitle"), description: t("idpGoogleDescription"), - icon: ( - {t("idpGoogleAlt")} - ) + icon: }, { id: "azure", title: t("idpAzureTitle"), description: t("idpAzureDescription"), - icon: ( - {t("idpAzureAlt")} - ) + icon: } ]; }, [hideTemplates, t]); diff --git a/src/components/ui/controlled-data-table.tsx b/src/components/ui/controlled-data-table.tsx index 34a35455c..1690d92a8 100644 --- a/src/components/ui/controlled-data-table.tsx +++ b/src/components/ui/controlled-data-table.tsx @@ -18,12 +18,14 @@ import { TableRow } from "@/components/ui/table"; import { DataTablePagination } from "@app/components/DataTablePagination"; +import type { DataTableAddAction } from "@app/components/ui/data-table"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger @@ -31,7 +33,14 @@ import { import { Input } from "@app/components/ui/input"; import { useStoredColumnVisibility } from "@app/hooks/useStoredColumnVisibility"; -import { Columns, Filter, Plus, RefreshCw, Search } from "lucide-react"; +import { + ChevronDown, + Columns, + Filter, + Plus, + RefreshCw, + Search +} from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; @@ -67,6 +76,8 @@ type ControlledDataTableProps = { tableId: string; addButtonText?: string; onAdd?: () => void; + addActions?: DataTableAddAction[]; + addButtonDisabled?: boolean; onRefresh?: () => void; isRefreshing?: boolean; refreshButtonDisabled?: boolean; @@ -90,6 +101,8 @@ export function ControlledDataTable({ rows, addButtonText, onAdd, + addActions, + addButtonDisabled = false, onRefresh, isRefreshing, refreshButtonDisabled = false, @@ -348,16 +361,49 @@ export function ControlledDataTable({
)} - {onAdd && addButtonText && ( + {addActions && addActions.length > 0 && addButtonText ? (
- + + + + + + {addActions.map((action, i) => ( + + action.onSelect() + } + > + {action.label} + + ))} + +
+ ) : ( + onAdd && + addButtonText && ( +
+ +
+ ) )}
diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index c62afd329..cf252f3ea 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -33,7 +33,7 @@ import { Button } from "@app/components/ui/button"; import { useEffect, useMemo, useRef, useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; -import { Plus, Search, RefreshCw, Columns, Filter } from "lucide-react"; +import { ChevronDown, Plus, Search, RefreshCw, Columns, Filter } from "lucide-react"; import { Card, CardContent, @@ -46,6 +46,7 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger @@ -165,12 +166,20 @@ export type DataTablePaginationState = PaginationState & { export type DataTablePaginationUpdateFn = (newPage: PaginationState) => void; +/** When set (non-empty), replaces the single add button with a dropdown; `onAdd` is not used. */ +export type DataTableAddAction = { + label: string; + onSelect: () => void; +}; + type DataTableProps = { columns: ExtendedColumnDef[]; data: TData[]; title?: string; addButtonText?: string; onAdd?: () => void; + /** Prefer over `onAdd` when non-empty. */ + addActions?: DataTableAddAction[]; addButtonDisabled?: boolean; onRefresh?: () => void; isRefreshing?: boolean; @@ -205,6 +214,7 @@ export function DataTable({ title, addButtonText, onAdd, + addActions, addButtonDisabled = false, onRefresh, isRefreshing, @@ -637,13 +647,45 @@ export function DataTable({
)} - {onAdd && addButtonText && ( + {addActions && addActions.length > 0 && addButtonText ? (
- + + + + + + {addActions.map((action, i) => ( + + action.onSelect() + } + > + {action.label} + + ))} + +
+ ) : ( + onAdd && + addButtonText && ( +
+ +
+ ) )}
diff --git a/src/lib/queries.ts b/src/lib/queries.ts index c4b0a4bce..e664009a4 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -1,7 +1,8 @@ import { build } from "@server/build"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { ListClientsResponse } from "@server/routers/client"; -import type { ListDomainsResponse } from "@server/routers/domain"; +import type { ListDomainsResponse, GetDNSRecordsResponse } from "@server/routers/domain"; +import type { GetDomainResponse } from "@server/routers/domain/getDomain"; import type { GetResourceWhitelistResponse, ListResourceNamesResponse, @@ -608,3 +609,49 @@ export const approvalQueries = { } }) }; + +export const domainQueries = { + getDomain: ({ + orgId, + domainId + }: { + orgId: string; + domainId: string; + }) => + queryOptions({ + queryKey: ["ORG", orgId, "DOMAIN", domainId] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/domain/${domainId}`, { signal }); + return res.data.data; + }, + refetchInterval: durationToMs(10, "seconds") + }), + getDNSRecords: ({ + orgId, + domainId + }: { + orgId: string; + domainId: string; + }) => + queryOptions({ + queryKey: [ + "ORG", + orgId, + "DOMAIN", + domainId, + "DNS_RECORDS" + ] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >( + `/org/${orgId}/domain/${domainId}/dns-records`, + { signal } + ); + return res.data.data; + }, + refetchInterval: durationToMs(10, "seconds") + }) +};