Merge branch 'alerting-rules' into trial

This commit is contained in:
Owen
2026-04-21 14:57:25 -07:00
220 changed files with 4948 additions and 1900 deletions
-17
View File
@@ -1,17 +0,0 @@
meta {
name: Create API Key
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/api-key
body: json
auth: inherit
}
body:json {
{
"isRoot": true
}
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Delete API Key
type: http
seq: 2
}
delete {
url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj
body: none
auth: inherit
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: List API Key Actions
type: http
seq: 6
}
get {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
body: none
auth: inherit
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: List Org API Keys
type: http
seq: 4
}
get {
url: http://localhost:3000/api/v1/org/home-lab/api-keys
body: none
auth: inherit
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: List Root API Keys
type: http
seq: 3
}
get {
url: http://localhost:3000/api/v1/root/api-keys
body: none
auth: inherit
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: Set API Key Actions
type: http
seq: 5
}
post {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
body: json
auth: inherit
}
body:json {
{
"actionIds": ["listSites"]
}
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: Set API Key Orgs
type: http
seq: 7
}
post {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs
body: json
auth: inherit
}
body:json {
{
"orgIds": ["home-lab"]
}
}
-3
View File
@@ -1,3 +0,0 @@
meta {
name: API Keys
}
-18
View File
@@ -1,18 +0,0 @@
meta {
name: 2fa-disable
type: http
seq: 6
}
post {
url: http://localhost:3000/api/v1/auth/2fa/disable
body: json
auth: none
}
body:json {
{
"password": "aaaaa-1A",
"code": "377289"
}
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: 2fa-enable
type: http
seq: 4
}
post {
url: http://localhost:3000/api/v1/auth/2fa/enable
body: json
auth: none
}
body:json {
{
"code": "374138"
}
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: 2fa-request
type: http
seq: 5
}
post {
url: http://localhost:3000/api/v1/auth/2fa/request
body: json
auth: none
}
body:json {
{
"password": "aaaaa-1A"
}
}
-18
View File
@@ -1,18 +0,0 @@
meta {
name: change-password
type: http
seq: 9
}
post {
url: http://localhost:3000/api/v1/auth/change-password
body: json
auth: none
}
body:json {
{
"oldPassword": "",
"newPassword": ""
}
}
-18
View File
@@ -1,18 +0,0 @@
meta {
name: login
type: http
seq: 1
}
post {
url: http://localhost:3000/api/v1/auth/login
body: json
auth: none
}
body:json {
{
"email": "admin@fosrl.io",
"password": "Password123!"
}
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: logout
type: http
seq: 3
}
post {
url: http://localhost:4000/api/v1/auth/logout
body: none
auth: none
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: reset-password-request
type: http
seq: 10
}
post {
url: http://localhost:3000/api/v1/auth/reset-password/request
body: json
auth: none
}
body:json {
{
"email": "milo@pangolin.net"
}
}
-19
View File
@@ -1,19 +0,0 @@
meta {
name: reset-password
type: http
seq: 11
}
post {
url: http://localhost:3000/api/v1/auth/reset-password
body: json
auth: none
}
body:json {
{
"token": "3uhsbom72dwdhboctwrtntyd6jrlg4jtf5oaxy4k",
"newPassword": "aaaaa-1A",
"code": "6irqCGR3"
}
}
-18
View File
@@ -1,18 +0,0 @@
meta {
name: signup
type: http
seq: 2
}
put {
url: http://localhost:3000/api/v1/auth/signup
body: json
auth: none
}
body:json {
{
"email": "numbat@pangolin.net",
"password": "Password123!"
}
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: verify-email-request
type: http
seq: 8
}
post {
url: http://localhost:3000/api/v1/auth/verify-email/request
body: none
auth: none
}
-17
View File
@@ -1,17 +0,0 @@
meta {
name: verify-email
type: http
seq: 7
}
post {
url: http://localhost:3000/api/v1/auth/verify-email
body: json
auth: none
}
body:json {
{
"code": "50317187"
}
}
-15
View File
@@ -1,15 +0,0 @@
meta {
name: verify-user
type: http
seq: 4
}
get {
url: http://localhost:3001/api/v1/badger/verify-user?sessionId=mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
body: none
auth: none
}
params:query {
sessionId: mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: createClient
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/site/1/client
body: json
auth: none
}
body:json {
{
"siteId": 1,
"name": "test",
"type": "olm",
"subnet": "100.90.129.4/30",
"olmId": "029yzunhx6nh3y5",
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
}
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: pickClientDefaults
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
body: none
auth: none
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Create OIDC Provider
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/org/home-lab/idp/oidc
body: json
auth: inherit
}
body:json {
{
"clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys",
"clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3",
"authUrl": "http://localhost:9000/application/o/authorize/",
"tokenUrl": "http://localhost:9000/application/o/token/",
"scopes": ["email", "openid", "profile"],
"userIdentifier": "email"
}
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Generate OIDC URL
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1
body: none
auth: inherit
}
-3
View File
@@ -1,3 +0,0 @@
meta {
name: IDP
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Traefik Config
type: http
seq: 1
}
get {
url: http://localhost:3001/api/v1/traefik-config
body: none
auth: inherit
}
-3
View File
@@ -1,3 +0,0 @@
meta {
name: Internal
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Create Newt
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/newt
body: none
auth: none
}
-18
View File
@@ -1,18 +0,0 @@
meta {
name: Get Token
type: http
seq: 1
}
get {
url: http://localhost:3000/api/v1/auth/newt/get-token
body: json
auth: none
}
body:json {
{
"newtId": "o0d4rdxq3stnz7b",
"secret": "sy7l09fnaesd03iwrfp9m3qf0ryn19g0zf3dqieaazb4k7vk"
}
}
-15
View File
@@ -1,15 +0,0 @@
meta {
name: createOlm
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/olm
body: none
auth: inherit
}
settings {
encodeUrl: true
}
-8
View File
@@ -1,8 +0,0 @@
meta {
name: Olm
seq: 15
}
auth {
mode: inherit
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Check Id
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/org/checkId
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: listOrgs
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
@@ -1,11 +0,0 @@
meta {
name: createRemoteExitNode
type: http
seq: 1
}
put {
url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: listResourcesByOrg
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
-16
View File
@@ -1,16 +0,0 @@
meta {
name: listResourcesBySite
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/site/1/resources?limit=10&offset=0
body: none
auth: none
}
params:query {
limit: 10
offset: 0
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Get Site
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/org/test/sites/mexican-mole-lizard-windy
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: listSites
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
-16
View File
@@ -1,16 +0,0 @@
meta {
name: listTargets
type: http
seq: 1
}
get {
url: http://localhost:3000/api/v1/resource/web.main.localhost/targets?limit=10&offset=0
body: none
auth: none
}
params:query {
limit: 10
offset: 0
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: Test
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1
body: none
auth: inherit
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: traefik-config
type: http
seq: 1
}
get {
url: http://localhost:3001/api/v1/traefik-config
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: adminListUsers
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/users
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: adminRemoveUser
type: http
seq: 3
}
delete {
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
body: none
auth: none
}
-11
View File
@@ -1,11 +0,0 @@
meta {
name: getUser
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
-13
View File
@@ -1,13 +0,0 @@
{
"version": "1",
"name": "Pangolin",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"presets": {
"requestType": "http",
"requestUrl": "http://localhost:3000/api/v1"
}
}
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Име за показване за този доставчик на идентичност", "idpDisplayName": "Име за показване за този доставчик на идентичност",
"idpAutoProvisionUsers": "Автоматично потребителско създаване", "idpAutoProvisionUsers": "Автоматично потребителско създаване",
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.", "idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
"idpAutoProvisionConfigureAfterCreate": "Можете да конфигурирате настройките за автоматично предоставяне, след като дистрибуторът на самоличност бъде създаден.",
"licenseBadge": "ЕЕ", "licenseBadge": "ЕЕ",
"idpType": "Тип доставчик", "idpType": "Тип доставчик",
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате", "idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Карта на роля по подразбиране", "defaultMappingsRole": "Карта на роля по подразбиране",
"defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.", "defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.",
"defaultMappingsOrg": "Карта на организация по подразбиране", "defaultMappingsOrg": "Карта на организация по подразбиране",
"defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.", "defaultMappingsOrgDescription": "При задаване, този израз трябва да върне идентификационния номер на организацията или true, за да се даде достъп на потребителя до тази организация. Ако не е зададено, дефинирането на роля е достатъчно: потребителят има право на достъп, стига валидно картографиране на роля да бъде разрешено за него в рамките на организацията.",
"defaultMappingsSubmit": "Запазване на файловете по подразбиране", "defaultMappingsSubmit": "Запазване на файловете по подразбиране",
"orgPoliciesEdit": "Редактиране на Организационна Политика", "orgPoliciesEdit": "Редактиране на Организационна Политика",
"org": "Организация", "org": "Организация",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Открит международен домейн", "internationaldomaindetected": "Открит международен домейн",
"willbestoredas": "Ще бъде съхранено като:", "willbestoredas": "Ще бъде съхранено като:",
"roleMappingDescription": "Определете как се разпределят ролите на потребителите при вписване, когато е активирано автоматично предоставяне.", "roleMappingDescription": "Определете как ролите се присвояват на потребителите, когато се вписват с този доставчик на самоличност.",
"selectRole": "Избор на роля", "selectRole": "Избор на роля",
"roleMappingExpression": "Израз", "roleMappingExpression": "Израз",
"selectRolePlaceholder": "Избор на роля", "selectRolePlaceholder": "Избор на роля",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно", "httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно",
"httpDestCreatedSuccess": "Дестинацията беше създадена успешно", "httpDestCreatedSuccess": "Дестинацията беше създадена успешно",
"httpDestUpdateFailed": "Неуспешно актуализиране на дестинацията", "httpDestUpdateFailed": "Неуспешно актуализиране на дестинацията",
"httpDestCreateFailed": "Неуспешно създаване на дестинацията" "httpDestCreateFailed": "Неуспешно създаване на дестинацията",
"idpAddActionCreateNew": "Създайте нов доставчик на самоличност",
"idpAddActionImportFromOrg": "Импортиране от друга организация",
"idpImportDialogTitle": "Импортиране на доставчик на самоличност",
"idpImportDialogDescription": "Изберете доставчик на самоличност от организация, в която сте администратор. Той ще бъде свързан с тази организация.",
"idpImportSearchPlaceholder": "Търсене по име на организация или доставчик...",
"idpImportEmpty": "Няма намерени доставчици на самоличност.",
"idpImportedDescription": "Доставчикът на самоличност беше импортиран успешно.",
"idpDeleteGlobalQuestion": "Сигурни ли сте, че искате да изтриете този доставчик на самоличност завинаги?",
"idpDeleteGlobalDescription": "Това ще изтрие доставичка на самоличност завинаги от всички организации, с които е свързан.",
"idpUnassociateTitle": "Отвързване на доставчик на самоличност",
"idpUnassociateQuestion": "Сигурни ли сте, че искате да отвържете този доставчик на самоличност от тази организация?",
"idpUnassociateDescription": "Всички потребители, свързани с този доставчик на самоличност, ще бъдат премахнати от тази организация, но доставчика на самоличност ще продължи да съществува за други свързани организации.",
"idpUnassociateConfirm": "Потвърдете отвързване на доставчика на самоличност",
"idpUnassociateWarning": "Това не може да бъде отменено за тази организация.",
"idpUnassociatedDescription": "Доставчика на самоличност е успешно отвързан от тази организация",
"idpUnassociateMenu": "Отвързване",
"idpDeleteAllOrgsMenu": "Изтриване"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity", "idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
"idpAutoProvisionUsers": "Automatická úprava uživatelů", "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.", "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", "licenseBadge": "PE",
"idpType": "Typ poskytovatele", "idpType": "Typ poskytovatele",
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat", "idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Výchozí mapování rolí", "defaultMappingsRole": "Výchozí mapování rolí",
"defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.", "defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.",
"defaultMappingsOrg": "Výchozí mapování organizace", "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í", "defaultMappingsSubmit": "Uložit výchozí mapování",
"orgPoliciesEdit": "Upravit zásady organizace", "orgPoliciesEdit": "Upravit zásady organizace",
"org": "Organizace", "org": "Organizace",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Zjištěna mezinárodní doména", "internationaldomaindetected": "Zjištěna mezinárodní doména",
"willbestoredas": "Bude uloženo jako:", "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", "selectRole": "Vyberte roli",
"roleMappingExpression": "Výraz", "roleMappingExpression": "Výraz",
"selectRolePlaceholder": "Vyberte roli", "selectRolePlaceholder": "Vyberte roli",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Cíl byl úspěšně aktualizován", "httpDestUpdatedSuccess": "Cíl byl úspěšně aktualizován",
"httpDestCreatedSuccess": "Cíl byl úspěšně vytvořen", "httpDestCreatedSuccess": "Cíl byl úspěšně vytvořen",
"httpDestUpdateFailed": "Nepodařilo se aktualizovat cíl", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter", "idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung", "idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.", "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", "licenseBadge": "EE",
"idpType": "Anbietertyp", "idpType": "Anbietertyp",
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten", "idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Standard-Rollenzuordnung", "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.", "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", "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", "defaultMappingsSubmit": "Standardzuordnungen speichern",
"orgPoliciesEdit": "Organisationsrichtlinie bearbeiten", "orgPoliciesEdit": "Organisationsrichtlinie bearbeiten",
"org": "Organisation", "org": "Organisation",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Internationale Domain erkannt", "internationaldomaindetected": "Internationale Domain erkannt",
"willbestoredas": "Wird gespeichert als:", "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", "selectRole": "Wählen Sie eine Rolle",
"roleMappingExpression": "Ausdruck", "roleMappingExpression": "Ausdruck",
"selectRolePlaceholder": "Rolle auswählen", "selectRolePlaceholder": "Rolle auswählen",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert", "httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
"httpDestCreatedSuccess": "Ziel erfolgreich erstellt", "httpDestCreatedSuccess": "Ziel erfolgreich erstellt",
"httpDestUpdateFailed": "Fehler beim Aktualisieren des Ziels", "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"
} }
+75 -26
View File
@@ -175,7 +175,7 @@
"proxyResourceTitle": "Manage Public Resources", "proxyResourceTitle": "Manage Public Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"proxyResourcesBannerTitle": "Web-based Public Access", "proxyResourcesBannerTitle": "Web-based Public Access",
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.", "proxyResourcesBannerDescription": "Public resources are HTTPS proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
"clientResourceTitle": "Manage Private Resources", "clientResourceTitle": "Manage Private Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access", "privateResourcesBannerTitle": "Zero-Trust Private Access",
@@ -392,7 +392,7 @@
"userTitle": "Manage All Users", "userTitle": "Manage All Users",
"userDescription": "View and manage all users in the system", "userDescription": "View and manage all users in the system",
"userAbount": "About User Management", "userAbount": "About User Management",
"userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.", "userAbountDescription": "This table displays all base user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their base user object. They will remain in the system. To completely remove a user from the system, you must delete their base user object using the delete action in this table.",
"userServer": "Server Users", "userServer": "Server Users",
"userSearch": "Search server users...", "userSearch": "Search server users...",
"userErrorDelete": "Error deleting user", "userErrorDelete": "Error deleting user",
@@ -535,7 +535,7 @@
"userSettings": "User Information", "userSettings": "User Information",
"userSettingsDescription": "Enter the details for the new user", "userSettingsDescription": "Enter the details for the new user",
"inviteEmailSent": "Send invite email to user", "inviteEmailSent": "Send invite email to user",
"inviteValid": "Valid For", "inviteValid": "Invite Valid For (days)",
"selectDuration": "Select duration", "selectDuration": "Select duration",
"selectResource": "Select Resource", "selectResource": "Select Resource",
"filterByResource": "Filter By Resource", "filterByResource": "Filter By Resource",
@@ -910,6 +910,7 @@
"idpDisplayName": "A display name for this identity provider", "idpDisplayName": "A display name for this identity provider",
"idpAutoProvisionUsers": "Auto Provision Users", "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.", "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", "licenseBadge": "EE",
"idpType": "Provider Type", "idpType": "Provider Type",
"idpTypeDescription": "Select the type of identity provider you want to configure", "idpTypeDescription": "Select the type of identity provider you want to configure",
@@ -961,7 +962,7 @@
"defaultMappingsRole": "Default Role Mapping", "defaultMappingsRole": "Default Role Mapping",
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.", "defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
"defaultMappingsOrg": "Default Organization Mapping", "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", "defaultMappingsSubmit": "Save Default Mappings",
"orgPoliciesEdit": "Edit Organization Policy", "orgPoliciesEdit": "Edit Organization Policy",
"org": "Organization", "org": "Organization",
@@ -1359,7 +1360,7 @@
"sidebarBillingAndLicenses": "Billing & Licenses", "sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics", "sidebarLogsAnalytics": "Analytics",
"alertingTitle": "Alerting", "alertingTitle": "Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications.", "alertingDescription": "Define sources, triggers, and actions for notifications",
"alertingRules": "Alert rules", "alertingRules": "Alert rules",
"alertingSearchRules": "Search rules…", "alertingSearchRules": "Search rules…",
"alertingAddRule": "Create Rule", "alertingAddRule": "Create Rule",
@@ -1383,6 +1384,18 @@
"alertingPickSites": "Sites", "alertingPickSites": "Sites",
"alertingPickHealthChecks": "Health checks", "alertingPickHealthChecks": "Health checks",
"alertingPickResources": "Resources", "alertingPickResources": "Resources",
"alertingAllSites": "All Sites",
"alertingAllSitesDescription": "Alert fires for any site",
"alertingSpecificSites": "Specific Sites",
"alertingSpecificSitesDescription": "Choose specific sites to watch",
"alertingAllHealthChecks": "All Health Checks",
"alertingAllHealthChecksDescription": "Alert fires for any health check",
"alertingSpecificHealthChecks": "Specific Health Checks",
"alertingSpecificHealthChecksDescription": "Choose specific health checks to watch",
"alertingAllResources": "All Resources",
"alertingAllResourcesDescription": "Alert fires for any resource",
"alertingSpecificResources": "Specific resources",
"alertingSpecificResourcesDescription": "Choose specific resources to watch",
"alertingSelectResources": "Select resources…", "alertingSelectResources": "Select resources…",
"alertingResourcesSelected": "{count} resources selected", "alertingResourcesSelected": "{count} resources selected",
"alertingResourcesEmpty": "No resources with targets in the first 10 results.", "alertingResourcesEmpty": "No resources with targets in the first 10 results.",
@@ -1390,12 +1403,27 @@
"alertingTrigger": "When to alert", "alertingTrigger": "When to alert",
"alertingTriggerSiteOnline": "Site online", "alertingTriggerSiteOnline": "Site online",
"alertingTriggerSiteOffline": "Site offline", "alertingTriggerSiteOffline": "Site offline",
"alertingTriggerSiteToggle": "Site status changes",
"alertingTriggerHcHealthy": "Health check healthy", "alertingTriggerHcHealthy": "Health check healthy",
"alertingTriggerHcUnhealthy": "Health check unhealthy", "alertingTriggerHcUnhealthy": "Health check unhealthy",
"alertingTriggerHcToggle": "Health check status changes",
"alertingTriggerResourceHealthy": "Resource healthy",
"alertingTriggerResourceUnhealthy": "Resource unhealthy",
"alertingSearchHealthChecks": "Search health checks…",
"alertingHealthChecksEmpty": "No health checks available.",
"alertingTriggerResourceToggle": "Resource status changes",
"alertingSourceResource": "Resource",
"alertingSectionActions": "Actions", "alertingSectionActions": "Actions",
"alertingAddAction": "Add action", "alertingAddAction": "Add action",
"alertingActionNotify": "Email", "alertingActionNotify": "Email",
"alertingActionNotifyDescription": "Send email notifications to users or roles",
"alertingActionWebhook": "Webhook", "alertingActionWebhook": "Webhook",
"alertingActionWebhookDescription": "Send an HTTP request to a custom endpoint",
"alertingExternalIntegration": "External Integration",
"alertingExternalPagerDutyDescription": "Send alerts to PagerDuty for incident management",
"alertingExternalOpsgenieDescription": "Route alerts to Opsgenie for on-call management",
"alertingExternalServiceNowDescription": "Create ServiceNow incidents from alert events",
"alertingExternalIncidentIoDescription": "Trigger Incident.io workflows from alert events",
"alertingActionType": "Action type", "alertingActionType": "Action type",
"alertingNotifyUsers": "Users", "alertingNotifyUsers": "Users",
"alertingNotifyRoles": "Roles", "alertingNotifyRoles": "Roles",
@@ -1418,12 +1446,15 @@
"alertingRolesSelected": "{count} roles selected", "alertingRolesSelected": "{count} roles selected",
"alertingSummarySites": "Sites ({count})", "alertingSummarySites": "Sites ({count})",
"alertingSummaryHealthChecks": "Health checks ({count})", "alertingSummaryHealthChecks": "Health checks ({count})",
"alertingSummaryResources": "Resources ({count})",
"alertingErrorNameRequired": "Enter a name", "alertingErrorNameRequired": "Enter a name",
"alertingErrorActionsMin": "Add at least one action", "alertingErrorActionsMin": "Add at least one action",
"alertingErrorPickSites": "Select at least one site", "alertingErrorPickSites": "Select at least one site",
"alertingErrorPickHealthChecks": "Select at least one health check", "alertingErrorPickHealthChecks": "Select at least one health check",
"alertingErrorPickResources": "Select at least one resource",
"alertingErrorTriggerSite": "Choose a site trigger", "alertingErrorTriggerSite": "Choose a site trigger",
"alertingErrorTriggerHealth": "Choose a health check trigger", "alertingErrorTriggerHealth": "Choose a health check trigger",
"alertingErrorTriggerResource": "Choose a resource trigger",
"alertingErrorNotifyRecipients": "Pick users, roles, or at least one email", "alertingErrorNotifyRecipients": "Pick users, roles, or at least one email",
"alertingConfigureSource": "Configure Source", "alertingConfigureSource": "Configure Source",
"alertingConfigureTrigger": "Configure Trigger", "alertingConfigureTrigger": "Configure Trigger",
@@ -2160,7 +2191,7 @@
}, },
"internationaldomaindetected": "International Domain Detected", "internationaldomaindetected": "International Domain Detected",
"willbestoredas": "Will be stored as:", "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", "selectRole": "Select a Role",
"roleMappingExpression": "Expression", "roleMappingExpression": "Expression",
"selectRolePlaceholder": "Choose a role", "selectRolePlaceholder": "Choose a role",
@@ -2549,7 +2580,7 @@
"action": "Action", "action": "Action",
"actor": "Actor", "actor": "Actor",
"timestamp": "Timestamp", "timestamp": "Timestamp",
"accessLogs": "Access Logs", "accessLogs": "Authentication Logs",
"exportCsv": "Export CSV", "exportCsv": "Export CSV",
"exportError": "Unknown error when exporting CSV", "exportError": "Unknown error when exporting CSV",
"exportCsvTooltip": "Within Time Range", "exportCsvTooltip": "Within Time Range",
@@ -2570,25 +2601,25 @@
"noMoreAuthMethods": "No Valid Auth", "noMoreAuthMethods": "No Valid Auth",
"ip": "IP", "ip": "IP",
"reason": "Reason", "reason": "Reason",
"requestLogs": "Request Logs", "requestLogs": "HTTPS Request Logs",
"requestAnalytics": "Request Analytics", "requestAnalytics": "Request Analytics",
"host": "Host", "host": "Host",
"location": "Location", "location": "Location",
"actionLogs": "Action Logs", "actionLogs": "Admin Action Logs",
"sidebarLogsRequest": "Request Logs", "sidebarLogsRequest": "HTTPS Request Logs",
"sidebarLogsAccess": "Access Logs", "sidebarLogsAccess": "Authentication Logs",
"sidebarLogsAction": "Action Logs", "sidebarLogsAction": "Admin Action Logs",
"logRetention": "Log Retention", "logRetention": "Log Retention",
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them", "logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
"requestLogsDescription": "View detailed request logs for resources in this organization", "requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization", "requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
"logRetentionRequestLabel": "Request Log Retention", "logRetentionRequestLabel": "HTTPS Request Log Retention",
"logRetentionRequestDescription": "How long to retain request logs", "logRetentionRequestDescription": "How long to retain request logs",
"logRetentionAccessLabel": "Access Log Retention", "logRetentionAccessLabel": "Authentication Log Retention",
"logRetentionAccessDescription": "How long to retain access logs", "logRetentionAccessDescription": "How long to retain access logs",
"logRetentionActionLabel": "Action Log Retention", "logRetentionActionLabel": "Admin Action Log Retention",
"logRetentionActionDescription": "How long to retain action logs", "logRetentionActionDescription": "How long to retain action logs",
"logRetentionConnectionLabel": "Connection Log Retention", "logRetentionConnectionLabel": "Network Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs", "logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Disabled", "logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days", "logRetention3Days": "3 days",
@@ -2600,10 +2631,10 @@
"logRetentionEndOfFollowingYear": "End of following year", "logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization", "actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization", "accessLogsDescription": "View access auth requests for resources in this organization",
"connectionLogs": "Connection Logs", "connectionLogs": "Network Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization", "connectionLogsDescription": "View network session logs handled by sites in this organization",
"sidebarLogsConnection": "Connection Logs", "sidebarLogsConnection": "Network Logs",
"sidebarLogsStreaming": "Streaming", "sidebarLogsStreaming": "Event Streaming",
"sourceAddress": "Source Address", "sourceAddress": "Source Address",
"destinationAddress": "Destination Address", "destinationAddress": "Destination Address",
"duration": "Duration", "duration": "Duration",
@@ -3035,13 +3066,13 @@
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.", "httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
"httpDestLogTypesTitle": "Log Types", "httpDestLogTypesTitle": "Log Types",
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.", "httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
"httpDestAccessLogsTitle": "Access Logs", "httpDestAccessLogsTitle": "Authentication Logs",
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.", "httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
"httpDestActionLogsTitle": "Action Logs", "httpDestActionLogsTitle": "Admin Action Logs",
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.", "httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
"httpDestConnectionLogsTitle": "Connection Logs", "httpDestConnectionLogsTitle": "Network Logs",
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.", "httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
"httpDestRequestLogsTitle": "Request Logs", "httpDestRequestLogsTitle": "HTTPS Request Logs",
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.", "httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
"httpDestSaveChanges": "Save Changes", "httpDestSaveChanges": "Save Changes",
"httpDestCreateDestination": "Create Destination", "httpDestCreateDestination": "Create Destination",
@@ -3060,5 +3091,23 @@
"healthCheckTabConnection": "Connection", "healthCheckTabConnection": "Connection",
"healthCheckTabAdvanced": "Advanced", "healthCheckTabAdvanced": "Advanced",
"healthCheckStrategyNotAvailable": "This strategy is not available. Please contact sales to enable this feature.", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad", "idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios", "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.", "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", "licenseBadge": "EE",
"idpType": "Tipo de proveedor", "idpType": "Tipo de proveedor",
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar", "idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Mapeo de Rol por defecto", "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.", "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", "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", "defaultMappingsSubmit": "Guardar asignaciones por defecto",
"orgPoliciesEdit": "Editar Política de Organización", "orgPoliciesEdit": "Editar Política de Organización",
"org": "Organización", "org": "Organización",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Dominio Internacional detectado", "internationaldomaindetected": "Dominio Internacional detectado",
"willbestoredas": "Se almacenará como:", "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", "selectRole": "Seleccione un rol",
"roleMappingExpression": "Expresión", "roleMappingExpression": "Expresión",
"selectRolePlaceholder": "Elija un rol", "selectRolePlaceholder": "Elija un rol",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Destino actualizado correctamente", "httpDestUpdatedSuccess": "Destino actualizado correctamente",
"httpDestCreatedSuccess": "Destino creado correctamente", "httpDestCreatedSuccess": "Destino creado correctamente",
"httpDestUpdateFailed": "Error al actualizar destino", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité", "idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs", "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.", "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", "licenseBadge": "EE",
"idpType": "Type de fournisseur", "idpType": "Type de fournisseur",
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer", "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", "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.", "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", "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", "defaultMappingsSubmit": "Enregistrer les mappages par défaut",
"orgPoliciesEdit": "Modifier la politique d'organisation", "orgPoliciesEdit": "Modifier la politique d'organisation",
"org": "Organisation", "org": "Organisation",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Domaine international détecté", "internationaldomaindetected": "Domaine international détecté",
"willbestoredas": "Sera stocké comme :", "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", "selectRole": "Sélectionnez un rôle",
"roleMappingExpression": "Expression", "roleMappingExpression": "Expression",
"selectRolePlaceholder": "Choisir un rôle", "selectRolePlaceholder": "Choisir un rôle",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Destination mise à jour avec succès", "httpDestUpdatedSuccess": "Destination mise à jour avec succès",
"httpDestCreatedSuccess": "Destination créée avec succès", "httpDestCreatedSuccess": "Destination créée avec succès",
"httpDestUpdateFailed": "Impossible de mettre à jour la destination", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Un nome visualizzato per questo provider di identità", "idpDisplayName": "Un nome visualizzato per questo provider di identità",
"idpAutoProvisionUsers": "Provisioning Automatico Utenti", "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.", "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", "licenseBadge": "EE",
"idpType": "Tipo di Provider", "idpType": "Tipo di Provider",
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare", "idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Mappatura Ruolo Predefinito", "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.", "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", "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", "defaultMappingsSubmit": "Salva Mappature Predefinite",
"orgPoliciesEdit": "Modifica Politica Organizzazione", "orgPoliciesEdit": "Modifica Politica Organizzazione",
"org": "Organizzazione", "org": "Organizzazione",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Dominio Internazionale Rilevato", "internationaldomaindetected": "Dominio Internazionale Rilevato",
"willbestoredas": "Verrà conservato come:", "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", "selectRole": "Seleziona un ruolo",
"roleMappingExpression": "Espressione", "roleMappingExpression": "Espressione",
"selectRolePlaceholder": "Scegli un ruolo", "selectRolePlaceholder": "Scegli un ruolo",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Destinazione aggiornata con successo", "httpDestUpdatedSuccess": "Destinazione aggiornata con successo",
"httpDestCreatedSuccess": "Destinazione creata con successo", "httpDestCreatedSuccess": "Destinazione creata con successo",
"httpDestUpdateFailed": "Impossibile aggiornare la destinazione", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "이 신원 공급자를 위한 표시 이름", "idpDisplayName": "이 신원 공급자를 위한 표시 이름",
"idpAutoProvisionUsers": "사용자 자동 프로비저닝", "idpAutoProvisionUsers": "사용자 자동 프로비저닝",
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
"idpAutoProvisionConfigureAfterCreate": "아이덴티티 공급자가 생성되면 자동 프로비저닝 설정을 구성할 수 있습니다.",
"licenseBadge": "EE", "licenseBadge": "EE",
"idpType": "제공자 유형", "idpType": "제공자 유형",
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "기본 역할 매핑", "defaultMappingsRole": "기본 역할 매핑",
"defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.", "defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.",
"defaultMappingsOrg": "기본 조직 매핑", "defaultMappingsOrg": "기본 조직 매핑",
"defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다.", "defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다. 설정되지 않으면, 역할 매핑 정의가 충분합니다: 사용자는 유효한 역할 매핑이 해석되는 한 조직에 허용됩니다.",
"defaultMappingsSubmit": "기본 매핑 저장", "defaultMappingsSubmit": "기본 매핑 저장",
"orgPoliciesEdit": "조직 정책 편집", "orgPoliciesEdit": "조직 정책 편집",
"org": "조직", "org": "조직",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "국제 도메인 감지됨", "internationaldomaindetected": "국제 도메인 감지됨",
"willbestoredas": "다음으로 저장됩니다:", "willbestoredas": "다음으로 저장됩니다:",
"roleMappingDescription": "자동 프로비저닝이 활성화되면 사용자가 로그인할 때 역할이 할당되는 방법을 결정합니다.", "roleMappingDescription": "사용자가 이 아이덴티티 공급자로 로그인할 때 역할이 할당되는 방법을 결정합니다.",
"selectRole": "역할 선택", "selectRole": "역할 선택",
"roleMappingExpression": "표현식", "roleMappingExpression": "표현식",
"selectRolePlaceholder": "역할 선택", "selectRolePlaceholder": "역할 선택",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다", "httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다",
"httpDestCreatedSuccess": "대상지가 성공적으로 생성되었습니다", "httpDestCreatedSuccess": "대상지가 성공적으로 생성되었습니다",
"httpDestUpdateFailed": "대상지를 업데이트하는 데 실패했습니다", "httpDestUpdateFailed": "대상지를 업데이트하는 데 실패했습니다",
"httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다" "httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다",
"idpAddActionCreateNew": "새로운 아이덴티티 공급자 생성",
"idpAddActionImportFromOrg": "다른 조직에서 가져오기",
"idpImportDialogTitle": "아이덴티티 공급자 가져오기",
"idpImportDialogDescription": "관리자인 조직에서 아이덴티티 공급자를 선택하십시오. 이는 이 조직에 연결됩니다.",
"idpImportSearchPlaceholder": "조직 또는 공급자 이름으로 검색...",
"idpImportEmpty": "아이덴티티 공급자를 찾을 수 없습니다.",
"idpImportedDescription": "아이덴티티 공급자가 성공적으로 가져왔습니다.",
"idpDeleteGlobalQuestion": "정말로 이 아이덴티티 공급자를 영구적으로 삭제하시겠습니까?",
"idpDeleteGlobalDescription": "이것은 연관된 모든 조직에서 아이덴티티 공급자를 영구적으로 삭제합니다.",
"idpUnassociateTitle": "아이덴티티 공급자의 연관 해제",
"idpUnassociateQuestion": "정말로 이 조직에서 이 아이덴티티 공급자의 연관을 해제하시겠습니까?",
"idpUnassociateDescription": "이 아이덴티티 공급자와 연관된 모든 사용자는 이 조직에서 제거될 것이지만, 아이덴티티 공급자는 다른 연관된 조직에 계속해서 존재할 것입니다.",
"idpUnassociateConfirm": "아이덴티티 공급자 연관 해제 확인",
"idpUnassociateWarning": "이 조직에서 이것은 되돌릴 수 없습니다.",
"idpUnassociatedDescription": "아이덴티티 공급자가 이 조직에서 성공적으로 연관 해제되었습니다",
"idpUnassociateMenu": "연관 해제",
"idpDeleteAllOrgsMenu": "삭제"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
"idpAutoProvisionUsers": "Automatisk brukerklargjøring", "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.", "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", "licenseBadge": "EE",
"idpType": "Leverandørtype", "idpType": "Leverandørtype",
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Standard rolletilordning", "defaultMappingsRole": "Standard rolletilordning",
"defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.",
"defaultMappingsOrg": "Standard organisasjonstilordning", "defaultMappingsOrg": "Standard organisasjonstilordning",
"defaultMappingsOrgDescription": "Dette uttrykket 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", "defaultMappingsSubmit": "Lagre standard tilordninger",
"orgPoliciesEdit": "Rediger Organisasjonspolicy", "orgPoliciesEdit": "Rediger Organisasjonspolicy",
"org": "Organisasjon", "org": "Organisasjon",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Internasjonalt domene oppdaget", "internationaldomaindetected": "Internasjonalt domene oppdaget",
"willbestoredas": "Vil bli lagret som:", "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", "selectRole": "Velg en rolle",
"roleMappingExpression": "Uttrykk", "roleMappingExpression": "Uttrykk",
"selectRolePlaceholder": "Velg en rolle", "selectRolePlaceholder": "Velg en rolle",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Målet er oppdatert", "httpDestUpdatedSuccess": "Målet er oppdatert",
"httpDestCreatedSuccess": "Målet er opprettet", "httpDestCreatedSuccess": "Målet er opprettet",
"httpDestUpdateFailed": "Kunne ikke oppdatere destinasjon", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider", "idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
"idpAutoProvisionUsers": "Auto Provisie Gebruikers", "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.", "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", "licenseBadge": "EE",
"idpType": "Type provider", "idpType": "Type provider",
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren", "idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Standaard Rol Toewijzing", "defaultMappingsRole": "Standaard Rol Toewijzing",
"defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.", "defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.",
"defaultMappingsOrg": "Standaard organisatie mapping", "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", "defaultMappingsSubmit": "Standaard toewijzingen opslaan",
"orgPoliciesEdit": "Organisatie beleid bewerken", "orgPoliciesEdit": "Organisatie beleid bewerken",
"org": "Organisatie", "org": "Organisatie",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Internationaal Domein Gedetecteerd", "internationaldomaindetected": "Internationaal Domein Gedetecteerd",
"willbestoredas": "Zal worden opgeslagen als:", "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", "selectRole": "Selecteer een rol",
"roleMappingExpression": "Expressie", "roleMappingExpression": "Expressie",
"selectRolePlaceholder": "Kies een rol", "selectRolePlaceholder": "Kies een rol",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt", "httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
"httpDestCreatedSuccess": "Bestemming succesvol aangemaakt", "httpDestCreatedSuccess": "Bestemming succesvol aangemaakt",
"httpDestUpdateFailed": "Bijwerken bestemming mislukt", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości", "idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników", "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.", "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", "licenseBadge": "EE",
"idpType": "Typ dostawcy", "idpType": "Typ dostawcy",
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować", "idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Domyślne mapowanie roli", "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.", "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", "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", "defaultMappingsSubmit": "Zapisz domyślne mapowania",
"orgPoliciesEdit": "Edytuj politykę organizacji", "orgPoliciesEdit": "Edytuj politykę organizacji",
"org": "Organizacja", "org": "Organizacja",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Wykryto międzynarodową domenę", "internationaldomaindetected": "Wykryto międzynarodową domenę",
"willbestoredas": "Będą przechowywane jako:", "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ę", "selectRole": "Wybierz rolę",
"roleMappingExpression": "Wyrażenie", "roleMappingExpression": "Wyrażenie",
"selectRolePlaceholder": "Wybierz rolę", "selectRolePlaceholder": "Wybierz rolę",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Cel został pomyślnie zaktualizowany", "httpDestUpdatedSuccess": "Cel został pomyślnie zaktualizowany",
"httpDestCreatedSuccess": "Cel został utworzony pomyślnie", "httpDestCreatedSuccess": "Cel został utworzony pomyślnie",
"httpDestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego", "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ń"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Um nome de exibição para este provedor de identidade", "idpDisplayName": "Um nome de exibição para este provedor de identidade",
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores", "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.", "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", "licenseBadge": "EE",
"idpType": "Tipo de Provedor", "idpType": "Tipo de Provedor",
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar", "idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Mapeamento de Função Padrão", "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.", "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", "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", "defaultMappingsSubmit": "Guardar Mapeamentos Padrão",
"orgPoliciesEdit": "Editar Política da Organização", "orgPoliciesEdit": "Editar Política da Organização",
"org": "Organização", "org": "Organização",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Domínio Internacional Detectado", "internationaldomaindetected": "Domínio Internacional Detectado",
"willbestoredas": "Será armazenado como:", "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", "selectRole": "Selecione uma função",
"roleMappingExpression": "Expressão", "roleMappingExpression": "Expressão",
"selectRolePlaceholder": "Escolha uma função", "selectRolePlaceholder": "Escolha uma função",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Destino atualizado com sucesso", "httpDestUpdatedSuccess": "Destino atualizado com sucesso",
"httpDestCreatedSuccess": "Destino criado com sucesso", "httpDestCreatedSuccess": "Destino criado com sucesso",
"httpDestUpdateFailed": "Falha ao atualizar destino", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений", "idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
"idpAutoProvisionUsers": "Автоматическое создание пользователей", "idpAutoProvisionUsers": "Автоматическое создание пользователей",
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.", "idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
"idpAutoProvisionConfigureAfterCreate": "Вы можете настроить параметры автоматического обеспечения после создания поставщика удостоверений.",
"licenseBadge": "EE", "licenseBadge": "EE",
"idpType": "Тип поставщика", "idpType": "Тип поставщика",
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить", "idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "Сопоставление ролей по умолчанию", "defaultMappingsRole": "Сопоставление ролей по умолчанию",
"defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.", "defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.",
"defaultMappingsOrg": "Сопоставление организаций по умолчанию", "defaultMappingsOrg": "Сопоставление организаций по умолчанию",
"defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.", "defaultMappingsOrgDescription": "При установке это выражение должно возвращать ID организации или true, чтобы пользователь мог получить доступ к этой организации. При отсутствии настройка отображения роли достаточно: пользователю разрешено войти, пока для него может быть решено отображение гарантированной роли в организации.",
"defaultMappingsSubmit": "Сохранить сопоставления по умолчанию", "defaultMappingsSubmit": "Сохранить сопоставления по умолчанию",
"orgPoliciesEdit": "Редактировать политику организации", "orgPoliciesEdit": "Редактировать политику организации",
"org": "Организация", "org": "Организация",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Обнаружен международный домен", "internationaldomaindetected": "Обнаружен международный домен",
"willbestoredas": "Будет храниться как:", "willbestoredas": "Будет храниться как:",
"roleMappingDescription": "Определите, как роли, назначаемые пользователям, когда они войдут в систему автоматического профиля.", "roleMappingDescription": "Определите, как роли присваиваются пользователям при входе с этим поставщиком удостоверений.",
"selectRole": "Выберите роль", "selectRole": "Выберите роль",
"roleMappingExpression": "Выражение", "roleMappingExpression": "Выражение",
"selectRolePlaceholder": "Выберите роль", "selectRolePlaceholder": "Выберите роль",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Адрес назначения успешно обновлен", "httpDestUpdatedSuccess": "Адрес назначения успешно обновлен",
"httpDestCreatedSuccess": "Адрес назначения успешно создан", "httpDestCreatedSuccess": "Адрес назначения успешно создан",
"httpDestUpdateFailed": "Не удалось обновить место назначения", "httpDestUpdateFailed": "Не удалось обновить место назначения",
"httpDestCreateFailed": "Не удалось создать место назначения" "httpDestCreateFailed": "Не удалось создать место назначения",
"idpAddActionCreateNew": "Создать нового поставщика удостоверений",
"idpAddActionImportFromOrg": "Импортировать из другой организации",
"idpImportDialogTitle": "Импортировать поставщика удостоверений",
"idpImportDialogDescription": "Выберите поставщика удостоверений из организации, где вы являетесь администратором. Он будет связан с этой организацией.",
"idpImportSearchPlaceholder": "Поиск по организации или имени поставщика...",
"idpImportEmpty": "Поставщики удостоверений не найдены.",
"idpImportedDescription": "Поставщик удостоверений успешно импортирован.",
"idpDeleteGlobalQuestion": "Вы уверены, что хотите навсегда удалить этого поставщика удостоверений?",
"idpDeleteGlobalDescription": "Это навсегда удалит поставщика удостоверений из всех организаций, с которыми он связан.",
"idpUnassociateTitle": "Рассоединить провайдера удостоверений",
"idpUnassociateQuestion": "Вы уверены, что хотите рассоединить этого поставщика удостоверений с этой организацией?",
"idpUnassociateDescription": "Все пользователи, связанные с этим поставщиком удостоверений, будут удалены из этой организации, но поставщик удостоверений будет продолжать существовать для других связанных организаций.",
"idpUnassociateConfirm": "Подтвердите рассоединение поставщика удостоверений",
"idpUnassociateWarning": "Это не может быть отменено для этой организации.",
"idpUnassociatedDescription": "Поставщик удостоверений успешно рассоединен с этой организацией",
"idpUnassociateMenu": "Рассоединить",
"idpDeleteAllOrgsMenu": "Удалить"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı", "idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla", "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.", "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": " ", "licenseBadge": " ",
"idpType": "Sağlayıcı Türü", "idpType": "Sağlayıcı Türü",
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin", "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", "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.", "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", "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", "defaultMappingsSubmit": "Varsayılan Eşlemeleri Kaydet",
"orgPoliciesEdit": "Kuruluş Politikasını Düzenle", "orgPoliciesEdit": "Kuruluş Politikasını Düzenle",
"org": "Kuruluş", "org": "Kuruluş",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi", "internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
"willbestoredas": "Şu şekilde depolanacak:", "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", "selectRole": "Bir Rol Seçin",
"roleMappingExpression": "İfade", "roleMappingExpression": "İfade",
"selectRolePlaceholder": "Bir rol seçin", "selectRolePlaceholder": "Bir rol seçin",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "Hedef başarıyla güncellendi", "httpDestUpdatedSuccess": "Hedef başarıyla güncellendi",
"httpDestCreatedSuccess": "Hedef başarıyla oluşturuldu", "httpDestCreatedSuccess": "Hedef başarıyla oluşturuldu",
"httpDestUpdateFailed": "Hedef güncellenemedi", "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"
} }
+21 -3
View File
@@ -898,6 +898,7 @@
"idpDisplayName": "此身份提供商的显示名称", "idpDisplayName": "此身份提供商的显示名称",
"idpAutoProvisionUsers": "自动提供用户", "idpAutoProvisionUsers": "自动提供用户",
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。", "idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
"idpAutoProvisionConfigureAfterCreate": "您可以在创建身份提供者后配置自动配置设置。",
"licenseBadge": "EE", "licenseBadge": "EE",
"idpType": "提供者类型", "idpType": "提供者类型",
"idpTypeDescription": "选择您想要配置的身份提供者类型", "idpTypeDescription": "选择您想要配置的身份提供者类型",
@@ -949,7 +950,7 @@
"defaultMappingsRole": "默认角色映射", "defaultMappingsRole": "默认角色映射",
"defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。", "defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。",
"defaultMappingsOrg": "默认组织映射", "defaultMappingsOrg": "默认组织映射",
"defaultMappingsOrgDescription": "此表达式必须返回 组织IDtrue 才能允许用户访问组织。", "defaultMappingsOrgDescription": "设置时,此表达式必须返回组织IDtrue才能用户访问组织。如果未设置,定义角色映射就足够了:只要在组织内可以为用户找出有效角色映射,用户就被允许进入。",
"defaultMappingsSubmit": "保存默认映射", "defaultMappingsSubmit": "保存默认映射",
"orgPoliciesEdit": "编辑组织策略", "orgPoliciesEdit": "编辑组织策略",
"org": "组织", "org": "组织",
@@ -2026,7 +2027,7 @@
}, },
"internationaldomaindetected": "检测到国际域", "internationaldomaindetected": "检测到国际域",
"willbestoredas": "储存为:", "willbestoredas": "储存为:",
"roleMappingDescription": "确定当用户启用自动配送时如何分配他们的角色。", "roleMappingDescription": "确定当用户使用此身份提供者登陆时如何分配角色。",
"selectRole": "选择角色", "selectRole": "选择角色",
"roleMappingExpression": "表达式", "roleMappingExpression": "表达式",
"selectRolePlaceholder": "选择角色", "selectRolePlaceholder": "选择角色",
@@ -2899,5 +2900,22 @@
"httpDestUpdatedSuccess": "目标已成功更新", "httpDestUpdatedSuccess": "目标已成功更新",
"httpDestCreatedSuccess": "目标创建成功", "httpDestCreatedSuccess": "目标创建成功",
"httpDestUpdateFailed": "更新目标失败", "httpDestUpdateFailed": "更新目标失败",
"httpDestCreateFailed": "创建目标失败" "httpDestCreateFailed": "创建目标失败",
"idpAddActionCreateNew": "创建新的身份提供者",
"idpAddActionImportFromOrg": "从另一个组织导入",
"idpImportDialogTitle": "导入身份提供者",
"idpImportDialogDescription": "从您是管理员的组织中选择一个身份提供者。它将关联到本组织。",
"idpImportSearchPlaceholder": "按组织或提供者名称搜索……",
"idpImportEmpty": "未找到身份提供者。",
"idpImportedDescription": "身份提供者已成功导入。",
"idpDeleteGlobalQuestion": "您确定要永久删除此身份提供者吗?",
"idpDeleteGlobalDescription": "这将永久删除与其关联的所有组织中的身份提供者。",
"idpUnassociateTitle": "取消关联身份提供者",
"idpUnassociateQuestion": "您确定要将此身份提供者从此组织中取消关联吗?",
"idpUnassociateDescription": "与此身份提供者关联的所有用户将从该组织中移除,但身份提供者仍会继续存在于关联的其他组织中。",
"idpUnassociateConfirm": "确认取消关联身份提供者",
"idpUnassociateWarning": "此操作无法对该组织撤销。",
"idpUnassociatedDescription": "身份提供者已成功从该组织中取消关联",
"idpUnassociateMenu": "取消关联",
"idpDeleteAllOrgsMenu": "删除"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+4 -1
View File
@@ -154,7 +154,10 @@ export enum ActionsEnum {
createHealthCheck = "createHealthCheck", createHealthCheck = "createHealthCheck",
updateHealthCheck = "updateHealthCheck", updateHealthCheck = "updateHealthCheck",
deleteHealthCheck = "deleteHealthCheck", deleteHealthCheck = "deleteHealthCheck",
listHealthChecks = "listHealthChecks" listHealthChecks = "listHealthChecks",
triggerSiteAlert = "triggerSiteAlert",
triggerResourceAlert = "triggerResourceAlert",
triggerHealthCheckAlert = "triggerHealthCheckAlert"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(
+21 -2
View File
@@ -21,6 +21,7 @@ import {
exitNodes, exitNodes,
sessions, sessions,
clients, clients,
resources,
siteResources, siteResources,
targetHealthCheck, targetHealthCheck,
sites sites
@@ -477,13 +478,21 @@ export const alertRules = pgTable("alertRules", {
.$type< .$type<
| "site_online" | "site_online"
| "site_offline" | "site_offline"
| "site_toggle"
| "health_check_healthy" | "health_check_healthy"
| "health_check_not_healthy" | "health_check_unhealthy"
| "health_check_toggle"
| "resource_healthy"
| "resource_unhealthy"
| "resource_toggle"
>() >()
.notNull(), .notNull(),
// Nullable depending on eventType // Nullable depending on eventType
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
cooldownSeconds: integer("cooldownSeconds").notNull().default(300), cooldownSeconds: integer("cooldownSeconds").notNull().default(300),
allSites: boolean("allSites").notNull().default(false),
allHealthChecks: boolean("allHealthChecks").notNull().default(false),
allResources: boolean("allResources").notNull().default(false),
lastTriggeredAt: bigint("lastTriggeredAt", { mode: "number" }), // nullable lastTriggeredAt: bigint("lastTriggeredAt", { mode: "number" }), // nullable
createdAt: bigint("createdAt", { mode: "number" }).notNull(), createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull() updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
@@ -509,6 +518,15 @@ export const alertHealthChecks = pgTable("alertHealthChecks", {
}) })
}); });
export const alertResources = pgTable("alertResources", {
alertRuleId: integer("alertRuleId")
.notNull()
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
});
// Separating channels by type avoids the mixed-shape problem entirely // Separating channels by type avoids the mixed-shape problem entirely
export const alertEmailActions = pgTable("alertEmailActions", { export const alertEmailActions = pgTable("alertEmailActions", {
emailActionId: serial("emailActionId").primaryKey(), emailActionId: serial("emailActionId").primaryKey(),
@@ -530,7 +548,7 @@ export const alertEmailRecipients = pgTable("alertEmailRecipients", {
userId: varchar("userId").references(() => users.userId, { userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade" onDelete: "cascade"
}), }),
roleId: varchar("roleId").references(() => roles.roleId, { roleId: integer("roleId").references(() => roles.roleId, {
onDelete: "cascade" onDelete: "cascade"
}), }),
email: varchar("email", { length: 255 }) // external emails not tied to a user email: varchar("email", { length: 255 }) // external emails not tied to a user
@@ -584,3 +602,4 @@ export type EventStreamingDestination = InferSelectModel<
export type EventStreamingCursor = InferSelectModel< export type EventStreamingCursor = InferSelectModel<
typeof eventStreamingCursors typeof eventStreamingCursors
>; >;
export type AlertResources = InferSelectModel<typeof alertResources>;
+3
View File
@@ -194,6 +194,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull(),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}).notNull(),
name: varchar("name"), name: varchar("name"),
hcEnabled: boolean("hcEnabled").notNull().default(false), hcEnabled: boolean("hcEnabled").notNull().default(false),
hcPath: varchar("hcPath"), hcPath: varchar("hcPath"),
+21 -2
View File
@@ -13,6 +13,7 @@ import {
domains, domains,
exitNodes, exitNodes,
orgs, orgs,
resources,
roles, roles,
sessions, sessions,
siteResources, siteResources,
@@ -469,12 +470,20 @@ export const alertRules = sqliteTable("alertRules", {
.$type< .$type<
| "site_online" | "site_online"
| "site_offline" | "site_offline"
| "site_toggle"
| "health_check_healthy" | "health_check_healthy"
| "health_check_not_healthy" | "health_check_unhealthy"
| "health_check_toggle"
| "resource_healthy"
| "resource_unhealthy"
| "resource_toggle"
>() >()
.notNull(), .notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
cooldownSeconds: integer("cooldownSeconds").notNull().default(300), cooldownSeconds: integer("cooldownSeconds").notNull().default(300),
allSites: integer("allSites", { mode: "boolean" }).notNull().default(false),
allHealthChecks: integer("allHealthChecks", { mode: "boolean" }).notNull().default(false),
allResources: integer("allResources", { mode: "boolean" }).notNull().default(false),
lastTriggeredAt: integer("lastTriggeredAt"), lastTriggeredAt: integer("lastTriggeredAt"),
createdAt: integer("createdAt").notNull(), createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt").notNull() updatedAt: integer("updatedAt").notNull()
@@ -500,6 +509,15 @@ export const alertHealthChecks = sqliteTable("alertHealthChecks", {
}) })
}); });
export const alertResources = sqliteTable("alertResources", {
alertRuleId: integer("alertRuleId")
.notNull()
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
});
export const alertEmailActions = sqliteTable("alertEmailActions", { export const alertEmailActions = sqliteTable("alertEmailActions", {
emailActionId: integer("emailActionId").primaryKey({ autoIncrement: true }), emailActionId: integer("emailActionId").primaryKey({ autoIncrement: true }),
alertRuleId: integer("alertRuleId") alertRuleId: integer("alertRuleId")
@@ -515,7 +533,7 @@ export const alertEmailRecipients = sqliteTable("alertEmailRecipients", {
.notNull() .notNull()
.references(() => alertEmailActions.emailActionId, { onDelete: "cascade" }), .references(() => alertEmailActions.emailActionId, { onDelete: "cascade" }),
userId: text("userId").references(() => users.userId, { onDelete: "cascade" }), userId: text("userId").references(() => users.userId, { onDelete: "cascade" }),
roleId: text("roleId").references(() => roles.roleId, { onDelete: "cascade" }), roleId: integer("roleId").references(() => roles.roleId, { onDelete: "cascade" }),
email: text("email") email: text("email")
}); });
@@ -561,3 +579,4 @@ export type EventStreamingDestination = InferSelectModel<
export type EventStreamingCursor = InferSelectModel< export type EventStreamingCursor = InferSelectModel<
typeof eventStreamingCursors typeof eventStreamingCursors
>; >;
export type AlertResources = InferSelectModel<typeof alertResources>;
+3
View File
@@ -217,6 +217,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull(),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}).notNull(),
name: text("name"), name: text("name"),
hcEnabled: integer("hcEnabled", { mode: "boolean" }) hcEnabled: integer("hcEnabled", { mode: "boolean" })
.notNull() .notNull()
+63 -2
View File
@@ -15,8 +15,13 @@ import {
export type AlertEventType = export type AlertEventType =
| "site_online" | "site_online"
| "site_offline" | "site_offline"
| "site_toggle"
| "health_check_healthy" | "health_check_healthy"
| "health_check_not_healthy"; | "health_check_unhealthy"
| "health_check_toggle"
| "resource_healthy"
| "resource_unhealthy"
| "resource_toggle";
interface Props { interface Props {
eventType: AlertEventType; eventType: AlertEventType;
@@ -50,6 +55,15 @@ function getEventMeta(eventType: AlertEventType): {
statusLabel: "Offline", statusLabel: "Offline",
statusColor: "#dc2626" statusColor: "#dc2626"
}; };
case "site_toggle":
return {
heading: "Site Status Changed",
previewText: "A site in your organization has changed status.",
summary:
"A site in your organization has changed status. Please review the details below and take action if needed.",
statusLabel: "Status Changed",
statusColor: "#f59e0b"
};
case "health_check_healthy": case "health_check_healthy":
return { return {
heading: "Health Check Recovered", heading: "Health Check Recovered",
@@ -60,7 +74,7 @@ function getEventMeta(eventType: AlertEventType): {
statusLabel: "Healthy", statusLabel: "Healthy",
statusColor: "#16a34a" statusColor: "#16a34a"
}; };
case "health_check_not_healthy": case "health_check_unhealthy":
return { return {
heading: "Health Check Failing", heading: "Health Check Failing",
previewText: previewText:
@@ -70,6 +84,53 @@ function getEventMeta(eventType: AlertEventType): {
statusLabel: "Not Healthy", statusLabel: "Not Healthy",
statusColor: "#dc2626" statusColor: "#dc2626"
}; };
case "health_check_toggle":
return {
heading: "Health Check Status Changed",
previewText:
"A health check in your organization has changed status.",
summary:
"A health check in your organization has changed status. Please review the details below and take action if needed.",
statusLabel: "Status Changed",
statusColor: "#f59e0b"
};
case "resource_healthy":
return {
heading: "Resource Healthy",
previewText: "A resource in your organization is now healthy.",
summary:
"A resource in your organization has recovered and is now reporting a healthy status.",
statusLabel: "Healthy",
statusColor: "#16a34a"
};
case "resource_unhealthy":
return {
heading: "Resource Unhealthy",
previewText: "A resource in your organization is not healthy.",
summary:
"A resource in your organization is currently unhealthy. Please review the details below and take action if needed.",
statusLabel: "Unhealthy",
statusColor: "#dc2626"
};
case "resource_toggle":
return {
heading: "Resource Status Changed",
previewText:
"A resource in your organization has changed status.",
summary:
"A resource in your organization has changed status. Please review the details below and take action if needed.",
statusLabel: "Status Changed",
statusColor: "#f59e0b"
};
default:
return {
heading: "Alert Notification",
previewText: "An alert event has occurred in your organization.",
summary:
"An alert event has occurred in your organization. Please review the details below and take action if needed.",
statusLabel: "Alert",
statusColor: "#f59e0b"
};
} }
} }
+2
View File
@@ -141,7 +141,9 @@ export async function updateProxyResources(
.insert(targetHealthCheck) .insert(targetHealthCheck)
.values({ .values({
name: `${targetData.hostname}:${targetData.port}`, name: `${targetData.hostname}:${targetData.port}`,
siteId: site.siteId,
targetId: newTarget.targetId, targetId: newTarget.targetId,
orgId: orgId,
hcEnabled: healthcheckData?.enabled || false, hcEnabled: healthcheckData?.enabled || false,
hcPath: healthcheckData?.path, hcPath: healthcheckData?.path,
hcScheme: healthcheckData?.scheme, hcScheme: healthcheckData?.scheme,
@@ -55,7 +55,7 @@ export async function fireHealthCheckHealthyAlert(
} }
/** /**
* Fire a `health_check_not_healthy` alert for the given health check. * Fire a `health_check_unhealthy` alert for the given health check.
* *
* Call this after a health check has been detected as failing so that any * Call this after a health check has been detected as failing so that any
* matching `alertRules` can dispatch their email and webhook actions. * matching `alertRules` can dispatch their email and webhook actions.
@@ -73,7 +73,7 @@ export async function fireHealthCheckNotHealthyAlert(
): Promise<void> { ): Promise<void> {
try { try {
await processAlerts({ await processAlerts({
eventType: "health_check_not_healthy", eventType: "health_check_unhealthy",
orgId, orgId,
healthCheckId, healthCheckId,
data: { data: {
@@ -19,73 +19,109 @@ import { processAlerts } from "../processAlerts";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* Fire a `health_check_healthy` alert for the given health check. * Fire a `resource_healthy` alert for the given resource.
* *
* Call this after a previously-failing health check has recovered so that any * Call this after a previously-unhealthy resource has recovered so that any
* matching `alertRules` can dispatch their email and webhook actions. * matching `alertRules` can dispatch their email and webhook actions.
* *
* @param orgId - Organisation that owns the health check. * @param orgId - Organisation that owns the resource.
* @param healthCheckId - Numeric primary key of the health check. * @param resourceId - Numeric primary key of the resource.
* @param healthCheckName - Human-readable name shown in notifications (optional). * @param resourceName - Human-readable name shown in notifications (optional).
* @param extra - Any additional key/value pairs to include in the payload. * @param extra - Any additional key/value pairs to include in the payload.
*/ */
export async function fireHealthCheckHealthyAlert( export async function fireResourceHealthyAlert(
orgId: string, orgId: string,
healthCheckId: number, resourceId: number,
healthCheckName?: string | null, resourceName?: string | null,
extra?: Record<string, unknown> extra?: Record<string, unknown>
): Promise<void> { ): Promise<void> {
try { try {
await processAlerts({ await processAlerts({
eventType: "health_check_healthy", eventType: "resource_healthy",
orgId, orgId,
healthCheckId, resourceId,
data: { data: {
healthCheckId, resourceId,
...(healthCheckName != null ? { healthCheckName } : {}), ...(resourceName != null ? { resourceName } : {}),
...extra ...extra
} }
}); });
} catch (err) { } catch (err) {
logger.error( logger.error(
`fireHealthCheckHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`, `fireResourceHealthyAlert: unexpected error for resourceId ${resourceId}`,
err err
); );
} }
} }
/** /**
* Fire a `health_check_not_healthy` alert for the given health check. * Fire a `resource_unhealthy` alert for the given resource.
* *
* Call this after a health check has been detected as failing so that any * Call this after a resource has been detected as unhealthy so that any
* matching `alertRules` can dispatch their email and webhook actions. * matching `alertRules` can dispatch their email and webhook actions.
* *
* @param orgId - Organisation that owns the health check. * @param orgId - Organisation that owns the resource.
* @param healthCheckId - Numeric primary key of the health check. * @param resourceId - Numeric primary key of the resource.
* @param healthCheckName - Human-readable name shown in notifications (optional). * @param resourceName - Human-readable name shown in notifications (optional).
* @param extra - Any additional key/value pairs to include in the payload. * @param extra - Any additional key/value pairs to include in the payload.
*/ */
export async function fireHealthCheckNotHealthyAlert( export async function fireResourceUnhealthyAlert(
orgId: string, orgId: string,
healthCheckId: number, resourceId: number,
healthCheckName?: string | null, resourceName?: string | null,
extra?: Record<string, unknown> extra?: Record<string, unknown>
): Promise<void> { ): Promise<void> {
try { try {
await processAlerts({ await processAlerts({
eventType: "health_check_not_healthy", eventType: "resource_unhealthy",
orgId, orgId,
healthCheckId, resourceId,
data: { data: {
healthCheckId, resourceId,
...(healthCheckName != null ? { healthCheckName } : {}), ...(resourceName != null ? { resourceName } : {}),
...extra ...extra
} }
}); });
} catch (err) { } catch (err) {
logger.error( logger.error(
`fireHealthCheckNotHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`, `fireResourceUnhealthyAlert: unexpected error for resourceId ${resourceId}`,
err err
); );
} }
} }
/**
* Fire a `resource_toggle` alert for the given resource.
*
* Call this when a resource's enabled/disabled status is toggled so that any
* matching `alertRules` can dispatch their email and webhook actions.
*
* @param orgId - Organisation that owns the resource.
* @param resourceId - Numeric primary key of the resource.
* @param resourceName - Human-readable name shown in notifications (optional).
* @param extra - Any additional key/value pairs to include in the payload.
*/
export async function fireResourceToggleAlert(
orgId: string,
resourceId: number,
resourceName?: string | null,
extra?: Record<string, unknown>
): Promise<void> {
try {
await processAlerts({
eventType: "resource_toggle",
orgId,
resourceId,
data: {
resourceId,
...(resourceName != null ? { resourceName } : {}),
...extra
}
});
} catch (err) {
logger.error(
`fireResourceToggleAlert: unexpected error for resourceId ${resourceId}`,
err
);
}
}
+51 -12
View File
@@ -11,12 +11,13 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { and, eq, isNull, or } from "drizzle-orm"; import { and, eq, or } from "drizzle-orm";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {
alertRules, alertRules,
alertSites, alertSites,
alertHealthChecks, alertHealthChecks,
alertResources,
alertEmailActions, alertEmailActions,
alertEmailRecipients, alertEmailRecipients,
alertWebhookActions, alertWebhookActions,
@@ -48,11 +49,9 @@ export async function processAlerts(context: AlertContext): Promise<void> {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 1. Find matching alert rules // 1. Find matching alert rules
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Rules with no junction-table entries match ALL sites / health checks. // Rules with allSites / allHealthChecks / allResources set to true match
// Rules with junction entries match only those specific IDs. // ANY event of that type. Rules without these flags set match only the
// We implement this with a LEFT JOIN: a NULL join result means the rule // specific IDs listed in the junction tables.
// has no scope restrictions (match all); a non-NULL result that satisfies
// the id equality filter means an explicit match.
const baseConditions = and( const baseConditions = and(
eq(alertRules.orgId, context.orgId), eq(alertRules.orgId, context.orgId),
eq(alertRules.eventType, context.eventType), eq(alertRules.eventType, context.eventType),
@@ -73,12 +72,20 @@ export async function processAlerts(context: AlertContext): Promise<void> {
and( and(
baseConditions, baseConditions,
or( or(
eq(alertSites.siteId, context.siteId), eq(alertRules.allSites, true),
isNull(alertSites.alertRuleId) eq(alertSites.siteId, context.siteId)
) )
) )
); );
rules = rows.map((r) => r.alertRules); // Deduplicate in case a rule matched on multiple junction rows
const seen = new Set<number>();
rules = rows
.map((r) => r.alertRules)
.filter((r) => {
if (seen.has(r.alertRuleId)) return false;
seen.add(r.alertRuleId);
return true;
});
} else if (context.healthCheckId != null) { } else if (context.healthCheckId != null) {
const rows = await db const rows = await db
.select() .select()
@@ -91,12 +98,44 @@ export async function processAlerts(context: AlertContext): Promise<void> {
and( and(
baseConditions, baseConditions,
or( or(
eq(alertHealthChecks.healthCheckId, context.healthCheckId), eq(alertRules.allHealthChecks, true),
isNull(alertHealthChecks.alertRuleId) eq(alertHealthChecks.healthCheckId, context.healthCheckId)
) )
) )
); );
rules = rows.map((r) => r.alertRules); const seen = new Set<number>();
rules = rows
.map((r) => r.alertRules)
.filter((r) => {
if (seen.has(r.alertRuleId)) return false;
seen.add(r.alertRuleId);
return true;
});
} else if (context.resourceId != null) {
const rows = await db
.select()
.from(alertRules)
.leftJoin(
alertResources,
eq(alertResources.alertRuleId, alertRules.alertRuleId)
)
.where(
and(
baseConditions,
or(
eq(alertRules.allResources, true),
eq(alertResources.resourceId, context.resourceId)
)
)
);
const seen = new Set<number>();
rules = rows
.map((r) => r.alertRules)
.filter((r) => {
if (seen.has(r.alertRuleId)) return false;
seen.add(r.alertRuleId);
return true;
});
} else { } else {
rules = []; rules = [];
} }
+12 -2
View File
@@ -72,10 +72,20 @@ function buildSubject(context: AlertContext): string {
return "[Alert] Site Back Online"; return "[Alert] Site Back Online";
case "site_offline": case "site_offline":
return "[Alert] Site Offline"; return "[Alert] Site Offline";
case "site_toggle":
return "[Alert] Site Status Changed";
case "health_check_healthy": case "health_check_healthy":
return "[Alert] Health Check Recovered"; return "[Alert] Health Check Recovered";
case "health_check_not_healthy": case "health_check_unhealthy":
return "[Alert] Health Check Failing"; return "[Alert] Health Check Failing";
case "health_check_toggle":
return "[Alert] Health Check Status Changed";
case "resource_healthy":
return "[Alert] Resource Healthy";
case "resource_unhealthy":
return "[Alert] Resource Unhealthy";
case "resource_toggle":
return "[Alert] Resource Status Changed";
default: { default: {
// Exhaustiveness fallback should never be reached with a // Exhaustiveness fallback should never be reached with a
// well-typed caller, but keeps runtime behaviour predictable. // well-typed caller, but keeps runtime behaviour predictable.
@@ -84,4 +94,4 @@ function buildSubject(context: AlertContext): string {
return "[Alert] Event Notification"; return "[Alert] Event Notification";
} }
} }
} }
+9 -2
View File
@@ -18,8 +18,13 @@
export type AlertEventType = export type AlertEventType =
| "site_online" | "site_online"
| "site_offline" | "site_offline"
| "site_toggle"
| "health_check_healthy" | "health_check_healthy"
| "health_check_not_healthy"; | "health_check_unhealthy"
| "health_check_toggle"
| "resource_healthy"
| "resource_unhealthy"
| "resource_toggle";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Webhook authentication config (stored as encrypted JSON in the DB) // Webhook authentication config (stored as encrypted JSON in the DB)
@@ -58,6 +63,8 @@ export interface AlertContext {
siteId?: number; siteId?: number;
/** Set for health_check_* events */ /** Set for health_check_* events */
healthCheckId?: number; healthCheckId?: number;
/** Set for resource_* events */
resourceId?: number;
/** Human-readable context data included in emails and webhook payloads */ /** Human-readable context data included in emails and webhook payloads */
data: Record<string, unknown>; data: Record<string, unknown>;
} }
@@ -0,0 +1,16 @@
/*
* 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.
*/
export * from "./triggerSiteAlert";
export * from "./triggerResourceAlert";
export * from "./triggerHealthCheckAlert";
@@ -0,0 +1,129 @@
/*
* 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 { targetHealthCheck, statusHistory } 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 { eq, and } from "drizzle-orm";
import {
fireHealthCheckHealthyAlert,
fireHealthCheckNotHealthyAlert
} from "#private/lib/alerts/events/healthCheckEvents";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
healthCheckId: z.coerce.number().int().positive()
});
const bodySchema = z.strictObject({
eventType: z.enum(["health_check_healthy", "health_check_unhealthy"])
});
export type TriggerHealthCheckAlertResponse = {
success: true;
};
export async function triggerHealthCheckAlert(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, healthCheckId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { eventType } = parsedBody.data;
// Verify the health check exists and belongs to the org
const [healthCheck] = await db
.select()
.from(targetHealthCheck)
.where(
and(
eq(
targetHealthCheck.targetHealthCheckId,
healthCheckId
),
eq(targetHealthCheck.orgId, orgId)
)
)
.limit(1);
if (!healthCheck) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Health check ${healthCheckId} not found in organization ${orgId}`
)
);
}
await db.insert(statusHistory).values({
entityType: "healthCheck",
entityId: healthCheckId,
orgId,
status: eventType === "health_check_healthy" ? "healthy" : "unhealthy",
timestamp: Math.floor(Date.now() / 1000)
});
if (eventType === "health_check_healthy") {
await fireHealthCheckHealthyAlert(
orgId,
healthCheckId,
healthCheck.name ?? undefined
);
} else {
await fireHealthCheckNotHealthyAlert(
orgId,
healthCheckId,
healthCheck.name ?? undefined
);
}
return response<TriggerHealthCheckAlertResponse>(res, {
data: { success: true },
success: true,
error: false,
message: "Alert triggered successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,135 @@
/*
* 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 { resources, statusHistory } 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 { eq, and } from "drizzle-orm";
import {
fireResourceHealthyAlert,
fireResourceUnhealthyAlert,
fireResourceToggleAlert
} from "#private/lib/alerts/events/resourceEvents";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
resourceId: z.coerce.number().int().positive()
});
const bodySchema = z.strictObject({
eventType: z.enum(["resource_healthy", "resource_unhealthy", "resource_toggle"])
});
export type TriggerResourceAlertResponse = {
success: true;
};
export async function triggerResourceAlert(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, resourceId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { eventType } = parsedBody.data;
// Verify the resource exists and belongs to the org
const [resource] = await db
.select()
.from(resources)
.where(
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
)
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource ${resourceId} not found in organization ${orgId}`
)
);
}
if (eventType === "resource_healthy" || eventType === "resource_unhealthy") {
await db.insert(statusHistory).values({
entityType: "resource",
entityId: resourceId,
orgId,
status: eventType === "resource_healthy" ? "healthy" : "unhealthy",
timestamp: Math.floor(Date.now() / 1000)
});
}
if (eventType === "resource_healthy") {
await fireResourceHealthyAlert(
orgId,
resourceId,
resource.name ?? undefined
);
} else if (eventType === "resource_unhealthy") {
await fireResourceUnhealthyAlert(
orgId,
resourceId,
resource.name ?? undefined
);
} else {
await fireResourceToggleAlert(
orgId,
resourceId,
resource.name ?? undefined
);
}
return response<TriggerResourceAlertResponse>(res, {
data: { success: true },
success: true,
error: false,
message: "Alert triggered successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,113 @@
/*
* 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 { sites, statusHistory } 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 { eq, and } from "drizzle-orm";
import {
fireSiteOnlineAlert,
fireSiteOfflineAlert
} from "#private/lib/alerts/events/siteEvents";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
siteId: z.coerce.number().int().positive()
});
const bodySchema = z.strictObject({
eventType: z.enum(["site_online", "site_offline"])
});
export type TriggerSiteAlertResponse = {
success: true;
};
export async function triggerSiteAlert(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, siteId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { eventType } = parsedBody.data;
// Verify the site exists and belongs to the org
const [site] = await db
.select()
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site ${siteId} not found in organization ${orgId}`
)
);
}
await db.insert(statusHistory).values({
entityType: "site",
entityId: siteId,
orgId,
status: eventType === "site_online" ? "online" : "offline",
timestamp: Math.floor(Date.now() / 1000)
});
if (eventType === "site_online") {
await fireSiteOnlineAlert(orgId, siteId, site.name ?? undefined);
} else {
await fireSiteOfflineAlert(orgId, siteId, site.name ?? undefined);
}
return response<TriggerSiteAlertResponse>(res, {
data: { success: true },
success: true,
error: false,
message: "Alert triggered successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -13,11 +13,12 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db, roles } from "@server/db";
import { import {
alertRules, alertRules,
alertSites, alertSites,
alertHealthChecks, alertHealthChecks,
alertResources,
alertEmailActions, alertEmailActions,
alertEmailRecipients, alertEmailRecipients,
alertWebhookActions alertWebhookActions
@@ -31,10 +32,16 @@ import { OpenAPITags, registry } from "@server/openApi";
import { encrypt } from "@server/lib/crypto"; import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
const SITE_EVENT_TYPES = ["site_online", "site_offline"] as const; export const SITE_EVENT_TYPES = ["site_online", "site_offline", "site_toggle"] as const;
const HC_EVENT_TYPES = [ export const HC_EVENT_TYPES = [
"health_check_healthy", "health_check_healthy",
"health_check_not_healthy" "health_check_unhealthy",
"health_check_toggle"
] as const;
export const RESOURCE_EVENT_TYPES = [
"resource_healthy",
"resource_unhealthy",
"resource_toggle"
] as const; ] as const;
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
@@ -51,22 +58,28 @@ const bodySchema = z
.strictObject({ .strictObject({
name: z.string().nonempty(), name: z.string().nonempty(),
eventType: z.enum([ eventType: z.enum([
"site_online", ...HC_EVENT_TYPES,
"site_offline", ...SITE_EVENT_TYPES,
"health_check_healthy", ...RESOURCE_EVENT_TYPES
"health_check_not_healthy"
]), ]),
enabled: z.boolean().optional().default(true), enabled: z.boolean().optional().default(true),
cooldownSeconds: z.number().int().nonnegative().optional().default(300), cooldownSeconds: z.number().int().nonnegative().optional().default(300),
// Source join tables - which is required depends on eventType // Source join tables - which is required depends on eventType
siteIds: z.array(z.number().int().positive()).optional().default([]), siteIds: z.array(z.number().int().positive()).optional().default([]),
allSites: z.boolean().optional().default(false),
healthCheckIds: z healthCheckIds: z
.array(z.number().int().positive()) .array(z.number().int().positive())
.optional() .optional()
.default([]), .default([]),
allHealthChecks: z.boolean().optional().default(false),
resourceIds: z
.array(z.number().int().positive())
.optional()
.default([]),
allResources: z.boolean().optional().default(false),
// Email recipients (flat) // Email recipients (flat)
userIds: z.array(z.string().nonempty()).optional().default([]), userIds: z.array(z.string().nonempty()).optional().default([]),
roleIds: z.array(z.string().nonempty()).optional().default([]), roleIds: z.array(z.number()).optional().default([]),
emails: z.array(z.string().email()).optional().default([]), emails: z.array(z.string().email()).optional().default([]),
// Webhook actions // Webhook actions
webhookActions: z.array(webhookActionSchema).optional().default([]) webhookActions: z.array(webhookActionSchema).optional().default([])
@@ -78,21 +91,23 @@ const bodySchema = z
const isHcEvent = (HC_EVENT_TYPES as readonly string[]).includes( const isHcEvent = (HC_EVENT_TYPES as readonly string[]).includes(
val.eventType val.eventType
); );
const isResourceEvent = (RESOURCE_EVENT_TYPES as readonly string[]).includes(
val.eventType
);
if (isSiteEvent && val.siteIds.length === 0) { if (isSiteEvent && !val.allSites && val.siteIds.length === 0) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: message: "At least one siteId is required for site event types when allSites is false",
"At least one siteId is required for site event types",
path: ["siteIds"] path: ["siteIds"]
}); });
} }
if (isHcEvent && val.healthCheckIds.length === 0) { if (isHcEvent && !val.allHealthChecks && val.healthCheckIds.length === 0) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: message:
"At least one healthCheckId is required for health check event types", "At least one healthCheckId is required for health check event types when allHealthChecks is false",
path: ["healthCheckIds"] path: ["healthCheckIds"]
}); });
} }
@@ -108,11 +123,50 @@ const bodySchema = z
if (isHcEvent && val.siteIds.length > 0) { if (isHcEvent && val.siteIds.length > 0) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: message: "siteIds must not be set for health check event types",
"siteIds must not be set for health check event types",
path: ["siteIds"] path: ["siteIds"]
}); });
} }
if (isResourceEvent && !val.allResources && val.resourceIds.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "At least one resourceId is required for resource event types when allResources is false",
path: ["resourceIds"]
});
}
if (isResourceEvent && val.siteIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "siteIds must not be set for resource event types",
path: ["siteIds"]
});
}
if (isResourceEvent && val.healthCheckIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "healthCheckIds must not be set for resource event types",
path: ["healthCheckIds"]
});
}
if (isSiteEvent && val.resourceIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "resourceIds must not be set for site event types",
path: ["resourceIds"]
});
}
if (isHcEvent && val.resourceIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "resourceIds must not be set for health check event types",
path: ["resourceIds"]
});
}
}); });
export type CreateAlertRuleResponse = { export type CreateAlertRuleResponse = {
@@ -171,7 +225,11 @@ export async function createAlertRule(
enabled, enabled,
cooldownSeconds, cooldownSeconds,
siteIds, siteIds,
allSites,
healthCheckIds, healthCheckIds,
allHealthChecks,
resourceIds,
allResources,
userIds, userIds,
roleIds, roleIds,
emails, emails,
@@ -188,13 +246,16 @@ export async function createAlertRule(
eventType, eventType,
enabled, enabled,
cooldownSeconds, cooldownSeconds,
allSites,
allHealthChecks,
allResources,
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now
}) })
.returning(); .returning();
// Insert site associations // Insert site associations (skipped when allSites=true — empty junction = match all)
if (siteIds.length > 0) { if (!allSites && siteIds.length > 0) {
await db.insert(alertSites).values( await db.insert(alertSites).values(
siteIds.map((siteId) => ({ siteIds.map((siteId) => ({
alertRuleId: rule.alertRuleId, alertRuleId: rule.alertRuleId,
@@ -203,8 +264,8 @@ export async function createAlertRule(
); );
} }
// Insert health check associations // Insert health check associations (skipped when allHealthChecks=true)
if (healthCheckIds.length > 0) { if (!allHealthChecks && healthCheckIds.length > 0) {
await db.insert(alertHealthChecks).values( await db.insert(alertHealthChecks).values(
healthCheckIds.map((healthCheckId) => ({ healthCheckIds.map((healthCheckId) => ({
alertRuleId: rule.alertRuleId, alertRuleId: rule.alertRuleId,
@@ -213,10 +274,22 @@ export async function createAlertRule(
); );
} }
// Insert resource associations (skipped when allResources=true)
if (!allResources && resourceIds.length > 0) {
await db.insert(alertResources).values(
resourceIds.map((resourceId) => ({
alertRuleId: rule.alertRuleId,
resourceId
}))
);
}
// Create the email action pivot row and recipients if any recipients // Create the email action pivot row and recipients if any recipients
// were supplied (userIds, roleIds, or raw emails). // were supplied (userIds, roleIds, or raw emails).
const hasRecipients = const hasRecipients =
userIds.length > 0 || roleIds.length > 0 || emails.length > 0; userIds.length > 0 ||
roleIds.length > 0 ||
emails.length > 0;
if (hasRecipients) { if (hasRecipients) {
const [emailActionRow] = await db const [emailActionRow] = await db
@@ -228,7 +301,7 @@ export async function createAlertRule(
...userIds.map((userId) => ({ ...userIds.map((userId) => ({
emailActionId: emailActionRow.emailActionId, emailActionId: emailActionRow.emailActionId,
userId, userId,
roleId: null as string | null, roleId: null as number | null,
email: null as string | null email: null as string | null
})), })),
...roleIds.map((roleId) => ({ ...roleIds.map((roleId) => ({
@@ -240,7 +313,7 @@ export async function createAlertRule(
...emails.map((email) => ({ ...emails.map((email) => ({
emailActionId: emailActionRow.emailActionId, emailActionId: emailActionRow.emailActionId,
userId: null as string | null, userId: null as string | null,
roleId: null as string | null, roleId: null as number | null,
email email
})) }))
]; ];
@@ -254,7 +327,10 @@ export async function createAlertRule(
webhookActions.map((wa) => ({ webhookActions.map((wa) => ({
alertRuleId: rule.alertRuleId, alertRuleId: rule.alertRuleId,
webhookUrl: wa.webhookUrl, webhookUrl: wa.webhookUrl,
config: wa.config != null ? encrypt(wa.config, serverSecret) : null, config:
wa.config != null
? encrypt(wa.config, serverSecret)
: null,
enabled: wa.enabled enabled: wa.enabled
})) }))
); );
@@ -275,4 +351,4 @@ export async function createAlertRule(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );
} }
} }
@@ -18,6 +18,7 @@ import {
alertRules, alertRules,
alertSites, alertSites,
alertHealthChecks, alertHealthChecks,
alertResources,
alertEmailActions, alertEmailActions,
alertEmailRecipients, alertEmailRecipients,
alertWebhookActions alertWebhookActions
@@ -31,7 +32,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { WebhookAlertConfig } from "@server/lib/alerts/types"; import { WebhookAlertConfig } from "#private/lib/alerts/types";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -47,8 +48,13 @@ export type GetAlertRuleResponse = {
eventType: eventType:
| "site_online" | "site_online"
| "site_offline" | "site_offline"
| "site_toggle"
| "health_check_healthy" | "health_check_healthy"
| "health_check_not_healthy"; | "health_check_unhealthy"
| "health_check_toggle"
| "resource_healthy"
| "resource_unhealthy"
| "resource_toggle";
enabled: boolean; enabled: boolean;
cooldownSeconds: number; cooldownSeconds: number;
lastTriggeredAt: number | null; lastTriggeredAt: number | null;
@@ -56,10 +62,11 @@ export type GetAlertRuleResponse = {
updatedAt: number; updatedAt: number;
siteIds: number[]; siteIds: number[];
healthCheckIds: number[]; healthCheckIds: number[];
resourceIds: number[];
recipients: { recipients: {
recipientId: number; recipientId: number;
userId: string | null; userId: string | null;
roleId: string | null; roleId: number | null;
email: string | null; email: string | null;
}[]; }[];
webhookActions: { webhookActions: {
@@ -128,6 +135,12 @@ export async function getAlertRule(
.from(alertHealthChecks) .from(alertHealthChecks)
.where(eq(alertHealthChecks.alertRuleId, alertRuleId)); .where(eq(alertHealthChecks.alertRuleId, alertRuleId));
// Fetch resource associations
const resourceRows = await db
.select()
.from(alertResources)
.where(eq(alertResources.alertRuleId, alertRuleId));
// Resolve the single email action row for this rule, then collect all // Resolve the single email action row for this rule, then collect all
// recipients into a flat list. The emailAction pivot row is an internal // recipients into a flat list. The emailAction pivot row is an internal
// implementation detail and is not surfaced to callers. // implementation detail and is not surfaced to callers.
@@ -175,26 +188,30 @@ export async function getAlertRule(
updatedAt: rule.updatedAt, updatedAt: rule.updatedAt,
siteIds: siteRows.map((r) => r.siteId), siteIds: siteRows.map((r) => r.siteId),
healthCheckIds: healthCheckRows.map((r) => r.healthCheckId), healthCheckIds: healthCheckRows.map((r) => r.healthCheckId),
resourceIds: resourceRows.map((r) => r.resourceId),
recipients, recipients,
webhookActions: webhooks.map((w) => { webhookActions: webhooks.map((w) => {
let parsedConfig: WebhookAlertConfig | null = null; let parsedConfig: WebhookAlertConfig | null = null;
if (w.config) { if (w.config) {
try { try {
const serverSecret = config.getRawConfig().server.secret!; const serverSecret =
const decrypted = decrypt(w.config, serverSecret); config.getRawConfig().server.secret!;
parsedConfig = JSON.parse(decrypted) as WebhookAlertConfig; const decrypted = decrypt(w.config, serverSecret);
} catch { parsedConfig = JSON.parse(
// best-effort return null if decryption fails decrypted
} ) as WebhookAlertConfig;
} } catch {
return { // best-effort return null if decryption fails
webhookActionId: w.webhookActionId, }
webhookUrl: w.webhookUrl, }
enabled: w.enabled, return {
lastSentAt: w.lastSentAt ?? null, webhookActionId: w.webhookActionId,
config: parsedConfig webhookUrl: w.webhookUrl,
}; enabled: w.enabled,
}) lastSentAt: w.lastSentAt ?? null,
config: parsedConfig
};
})
}, },
success: true, success: true,
error: false, error: false,
@@ -207,4 +224,4 @@ export async function getAlertRule(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );
} }
} }
@@ -14,14 +14,14 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { alertRules, alertSites, alertHealthChecks } from "@server/db"; import { alertRules, alertSites, alertHealthChecks, alertResources } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, like, sql } from "drizzle-orm";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -39,7 +39,18 @@ const querySchema = z.strictObject({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.number().int().nonnegative()) .pipe(z.number().int().nonnegative()),
query: z.string().optional(),
siteId: z
.string()
.optional()
.transform((v) => (v !== undefined ? Number(v) : undefined))
.pipe(z.number().int().positive().optional()),
resourceId: z
.string()
.optional()
.transform((v) => (v !== undefined ? Number(v) : undefined))
.pipe(z.number().int().positive().optional())
}); });
export type ListAlertRulesResponse = { export type ListAlertRulesResponse = {
@@ -55,6 +66,7 @@ export type ListAlertRulesResponse = {
updatedAt: number; updatedAt: number;
siteIds: number[]; siteIds: number[];
healthCheckIds: number[]; healthCheckIds: number[];
resourceIds: number[];
}[]; }[];
pagination: { pagination: {
total: number; total: number;
@@ -101,12 +113,69 @@ export async function listAlertRules(
) )
); );
} }
const { limit, offset } = parsedQuery.data; const { limit, offset, query, siteId, resourceId } = parsedQuery.data;
// Resolve siteId filter → matching alertRuleIds
let siteFilterRuleIds: number[] | null = null;
if (siteId !== undefined) {
const rows = await db
.select({ alertRuleId: alertSites.alertRuleId })
.from(alertSites)
.where(eq(alertSites.siteId, siteId));
siteFilterRuleIds = rows.map((r) => r.alertRuleId);
if (siteFilterRuleIds.length === 0) {
return response<ListAlertRulesResponse>(res, {
data: {
alertRules: [],
pagination: { total: 0, limit, offset }
},
success: true,
error: false,
message: "Alert rules retrieved successfully",
status: HttpCode.OK
});
}
}
// Resolve resourceId filter → matching alertRuleIds
let resourceFilterRuleIds: number[] | null = null;
if (resourceId !== undefined) {
const rows = await db
.select({ alertRuleId: alertResources.alertRuleId })
.from(alertResources)
.where(eq(alertResources.resourceId, resourceId));
resourceFilterRuleIds = rows.map((r) => r.alertRuleId);
if (resourceFilterRuleIds.length === 0) {
return response<ListAlertRulesResponse>(res, {
data: {
alertRules: [],
pagination: { total: 0, limit, offset }
},
success: true,
error: false,
message: "Alert rules retrieved successfully",
status: HttpCode.OK
});
}
}
const whereClause = and(
eq(alertRules.orgId, orgId),
query
? like(sql`LOWER(${alertRules.name})`, `%${query.toLowerCase()}%`)
: undefined,
siteFilterRuleIds !== null
? inArray(alertRules.alertRuleId, siteFilterRuleIds)
: undefined,
resourceFilterRuleIds !== null
? inArray(alertRules.alertRuleId, resourceFilterRuleIds)
: undefined
);
const list = await db const list = await db
.select() .select()
.from(alertRules) .from(alertRules)
.where(eq(alertRules.orgId, orgId)) .where(whereClause)
.orderBy(sql`${alertRules.createdAt} DESC`) .orderBy(sql`${alertRules.createdAt} DESC`)
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
@@ -114,7 +183,7 @@ export async function listAlertRules(
const [{ count }] = await db const [{ count }] = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(alertRules) .from(alertRules)
.where(eq(alertRules.orgId, orgId)); .where(whereClause);
// Batch-fetch site and health-check associations for all returned rules // Batch-fetch site and health-check associations for all returned rules
// in two queries rather than N+1 individual lookups. // in two queries rather than N+1 individual lookups.
@@ -138,6 +207,14 @@ export async function listAlertRules(
) )
: []; : [];
const resourceRows =
ruleIds.length > 0
? await db
.select()
.from(alertResources)
.where(inArray(alertResources.alertRuleId, ruleIds))
: [];
// Index by alertRuleId for O(1) lookup when building the response // Index by alertRuleId for O(1) lookup when building the response
const sitesByRule = new Map<number, number[]>(); const sitesByRule = new Map<number, number[]>();
for (const row of siteRows) { for (const row of siteRows) {
@@ -153,6 +230,13 @@ export async function listAlertRules(
healthChecksByRule.set(row.alertRuleId, existing); healthChecksByRule.set(row.alertRuleId, existing);
} }
const resourcesByRule = new Map<number, number[]>();
for (const row of resourceRows) {
const existing = resourcesByRule.get(row.alertRuleId) ?? [];
existing.push(row.resourceId);
resourcesByRule.set(row.alertRuleId, existing);
}
return response<ListAlertRulesResponse>(res, { return response<ListAlertRulesResponse>(res, {
data: { data: {
alertRules: list.map((rule) => ({ alertRules: list.map((rule) => ({
@@ -167,7 +251,8 @@ export async function listAlertRules(
updatedAt: rule.updatedAt, updatedAt: rule.updatedAt,
siteIds: sitesByRule.get(rule.alertRuleId) ?? [], siteIds: sitesByRule.get(rule.alertRuleId) ?? [],
healthCheckIds: healthCheckIds:
healthChecksByRule.get(rule.alertRuleId) ?? [] healthChecksByRule.get(rule.alertRuleId) ?? [],
resourceIds: resourcesByRule.get(rule.alertRuleId) ?? []
})), })),
pagination: { pagination: {
total: count, total: count,
@@ -18,6 +18,7 @@ import {
alertRules, alertRules,
alertSites, alertSites,
alertHealthChecks, alertHealthChecks,
alertResources,
alertEmailActions, alertEmailActions,
alertEmailRecipients, alertEmailRecipients,
alertWebhookActions alertWebhookActions
@@ -31,12 +32,8 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto"; import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { HC_EVENT_TYPES, SITE_EVENT_TYPES, RESOURCE_EVENT_TYPES } from "./createAlertRule";
const SITE_EVENT_TYPES = ["site_online", "site_offline"] as const; import { invalidateAllRemoteExitNodeSessions } from "@server/private/auth/sessions/remoteExitNode";
const HC_EVENT_TYPES = [
"health_check_healthy",
"health_check_not_healthy"
] as const;
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -57,20 +54,23 @@ const bodySchema = z
name: z.string().nonempty().optional(), name: z.string().nonempty().optional(),
eventType: z eventType: z
.enum([ .enum([
"site_online", ...HC_EVENT_TYPES,
"site_offline", ...SITE_EVENT_TYPES,
"health_check_healthy", ...RESOURCE_EVENT_TYPES
"health_check_not_healthy"
]) ])
.optional(), .optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
cooldownSeconds: z.number().int().nonnegative().optional(), cooldownSeconds: z.number().int().nonnegative().optional(),
// Source join tables - if provided the full set is replaced // Source join tables - if provided the full set is replaced
siteIds: z.array(z.number().int().positive()).optional(), siteIds: z.array(z.number().int().positive()).optional(),
allSites: z.boolean().optional(),
healthCheckIds: z.array(z.number().int().positive()).optional(), healthCheckIds: z.array(z.number().int().positive()).optional(),
allHealthChecks: z.boolean().optional(),
resourceIds: z.array(z.number().int().positive()).optional(),
allResources: z.boolean().optional(),
// Recipient arrays - if any are provided the full recipient set is replaced // Recipient arrays - if any are provided the full recipient set is replaced
userIds: z.array(z.string().nonempty()).optional(), userIds: z.array(z.string().nonempty()).optional(),
roleIds: z.array(z.string().nonempty()).optional(), roleIds: z.array(z.number()).optional(),
emails: z.array(z.string().email()).optional(), emails: z.array(z.string().email()).optional(),
// Webhook actions - if provided the full webhook set is replaced // Webhook actions - if provided the full webhook set is replaced
webhookActions: z.array(webhookActionSchema).optional() webhookActions: z.array(webhookActionSchema).optional()
@@ -84,6 +84,33 @@ const bodySchema = z
const isHcEvent = (HC_EVENT_TYPES as readonly string[]).includes( const isHcEvent = (HC_EVENT_TYPES as readonly string[]).includes(
val.eventType val.eventType
); );
const isResourceEvent = (RESOURCE_EVENT_TYPES as readonly string[]).includes(
val.eventType
);
if (isSiteEvent && val.siteIds !== undefined && val.siteIds.length === 0 && !val.allSites) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "At least one siteId is required for site event types when allSites is false",
path: ["siteIds"]
});
}
if (isHcEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length === 0 && !val.allHealthChecks) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "At least one healthCheckId is required for health check event types when allHealthChecks is false",
path: ["healthCheckIds"]
});
}
if (isResourceEvent && val.resourceIds !== undefined && val.resourceIds.length === 0 && !val.allResources) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "At least one resourceId is required for resource event types when allResources is false",
path: ["resourceIds"]
});
}
if (isSiteEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length > 0) { if (isSiteEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length > 0) {
ctx.addIssue({ ctx.addIssue({
@@ -100,6 +127,22 @@ const bodySchema = z
path: ["siteIds"] path: ["siteIds"]
}); });
} }
if (isResourceEvent && val.siteIds !== undefined && val.siteIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "siteIds must not be set for resource event types",
path: ["siteIds"]
});
}
if (isResourceEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "healthCheckIds must not be set for resource event types",
path: ["healthCheckIds"]
});
}
}); });
export type UpdateAlertRuleResponse = { export type UpdateAlertRuleResponse = {
@@ -174,7 +217,11 @@ export async function updateAlertRule(
enabled, enabled,
cooldownSeconds, cooldownSeconds,
siteIds, siteIds,
allSites,
healthCheckIds, healthCheckIds,
allHealthChecks,
resourceIds,
allResources,
userIds, userIds,
roleIds, roleIds,
emails, emails,
@@ -189,8 +236,10 @@ export async function updateAlertRule(
if (name !== undefined) updateData.name = name; if (name !== undefined) updateData.name = name;
if (eventType !== undefined) updateData.eventType = eventType; if (eventType !== undefined) updateData.eventType = eventType;
if (enabled !== undefined) updateData.enabled = enabled; if (enabled !== undefined) updateData.enabled = enabled;
if (cooldownSeconds !== undefined) if (cooldownSeconds !== undefined) updateData.cooldownSeconds = cooldownSeconds;
updateData.cooldownSeconds = cooldownSeconds; if (allSites !== undefined) updateData.allSites = allSites;
if (allHealthChecks !== undefined) updateData.allHealthChecks = allHealthChecks;
if (allResources !== undefined) updateData.allResources = allResources;
await db await db
.update(alertRules) .update(alertRules)
@@ -203,12 +252,14 @@ export async function updateAlertRule(
); );
// --- Full-replace site associations if siteIds was provided --- // --- Full-replace site associations if siteIds was provided ---
if (siteIds !== undefined) { if (siteIds !== undefined || allSites !== undefined) {
await db await db
.delete(alertSites) .delete(alertSites)
.where(eq(alertSites.alertRuleId, alertRuleId)); .where(eq(alertSites.alertRuleId, alertRuleId));
if (siteIds.length > 0) { // Only insert junction rows when allSites is not true
const effectiveAllSites = allSites ?? false;
if (!effectiveAllSites && siteIds !== undefined && siteIds.length > 0) {
await db.insert(alertSites).values( await db.insert(alertSites).values(
siteIds.map((siteId) => ({ siteIds.map((siteId) => ({
alertRuleId, alertRuleId,
@@ -219,12 +270,13 @@ export async function updateAlertRule(
} }
// --- Full-replace health check associations if healthCheckIds was provided --- // --- Full-replace health check associations if healthCheckIds was provided ---
if (healthCheckIds !== undefined) { if (healthCheckIds !== undefined || allHealthChecks !== undefined) {
await db await db
.delete(alertHealthChecks) .delete(alertHealthChecks)
.where(eq(alertHealthChecks.alertRuleId, alertRuleId)); .where(eq(alertHealthChecks.alertRuleId, alertRuleId));
if (healthCheckIds.length > 0) { const effectiveAllHealthChecks = allHealthChecks ?? false;
if (!effectiveAllHealthChecks && healthCheckIds !== undefined && healthCheckIds.length > 0) {
await db.insert(alertHealthChecks).values( await db.insert(alertHealthChecks).values(
healthCheckIds.map((healthCheckId) => ({ healthCheckIds.map((healthCheckId) => ({
alertRuleId, alertRuleId,
@@ -234,6 +286,23 @@ export async function updateAlertRule(
} }
} }
// --- Full-replace resource associations if resourceIds was provided ---
if (resourceIds !== undefined || allResources !== undefined) {
await db
.delete(alertResources)
.where(eq(alertResources.alertRuleId, alertRuleId));
const effectiveAllResources = allResources ?? false;
if (!effectiveAllResources && resourceIds !== undefined && resourceIds.length > 0) {
await db.insert(alertResources).values(
resourceIds.map((resourceId) => ({
alertRuleId,
resourceId
}))
);
}
}
// --- Full-replace recipients if any recipient array was provided --- // --- Full-replace recipients if any recipient array was provided ---
const recipientsProvided = const recipientsProvided =
userIds !== undefined || userIds !== undefined ||
@@ -244,7 +313,7 @@ export async function updateAlertRule(
const newRecipients = [ const newRecipients = [
...(userIds ?? []).map((userId) => ({ ...(userIds ?? []).map((userId) => ({
userId, userId,
roleId: null as string | null, roleId: null as number | null,
email: null as string | null email: null as string | null
})), })),
...(roleIds ?? []).map((roleId) => ({ ...(roleIds ?? []).map((roleId) => ({
@@ -254,7 +323,7 @@ export async function updateAlertRule(
})), })),
...(emails ?? []).map((email) => ({ ...(emails ?? []).map((email) => ({
userId: null as string | null, userId: null as string | null,
roleId: null as string | null, roleId: null as number | null,
email email
})) }))
]; ];
@@ -331,4 +400,4 @@ export async function updateAlertRule(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );
} }
} }
+35 -9
View File
@@ -42,7 +42,9 @@ import {
verifyRoleAccess, verifyRoleAccess,
verifyUserAccess, verifyUserAccess,
verifyUserCanSetUserOrgRoles, verifyUserCanSetUserOrgRoles,
verifySiteProvisioningKeyAccess verifySiteProvisioningKeyAccess,
verifyIsLoggedInUser,
verifyAdmin
} from "@server/middlewares"; } from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { import {
@@ -89,6 +91,7 @@ authenticated.put(
"/org/:orgId/idp/oidc", "/org/:orgId/idp/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc), verifyValidSubscription(tierMatrix.orgOidc),
orgIdp.requireOrgIdentityProviderMode,
verifyOrgAccess, verifyOrgAccess,
verifyLimits, verifyLimits,
verifyUserHasAction(ActionsEnum.createIdp), verifyUserHasAction(ActionsEnum.createIdp),
@@ -96,10 +99,23 @@ authenticated.put(
orgIdp.createOrgOidcIdp 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( authenticated.post(
"/org/:orgId/idp/:idpId/oidc", "/org/:orgId/idp/:idpId/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc), verifyValidSubscription(tierMatrix.orgOidc),
orgIdp.requireOrgIdentityProviderMode,
verifyOrgAccess, verifyOrgAccess,
verifyIdpAccess, verifyIdpAccess,
verifyLimits, verifyLimits,
@@ -111,6 +127,7 @@ authenticated.post(
authenticated.delete( authenticated.delete(
"/org/:orgId/idp/:idpId", "/org/:orgId/idp/:idpId",
verifyValidLicense, verifyValidLicense,
orgIdp.requireOrgIdentityProviderMode,
verifyOrgAccess, verifyOrgAccess,
verifyIdpAccess, verifyIdpAccess,
verifyUserHasAction(ActionsEnum.deleteIdp), verifyUserHasAction(ActionsEnum.deleteIdp),
@@ -118,6 +135,17 @@ authenticated.delete(
orgIdp.deleteOrgIdp orgIdp.deleteOrgIdp
); );
authenticated.delete(
"/org/:orgId/idp/:idpId/association",
verifyValidLicense,
orgIdp.requireOrgIdentityProviderMode,
verifyOrgAccess,
verifyIdpAccess,
verifyUserHasAction(ActionsEnum.deleteIdp),
logActionAudit(ActionsEnum.deleteIdp),
orgIdp.unassociateOrgIdp
);
authenticated.get( authenticated.get(
"/org/:orgId/idp/:idpId", "/org/:orgId/idp/:idpId",
verifyValidLicense, verifyValidLicense,
@@ -127,16 +155,14 @@ authenticated.get(
orgIdp.getOrgIdp 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("/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( authenticated.get(
"/org/:orgId/certificate/:domainId/:domain", "/org/:orgId/certificate/:domainId/:domain",
verifyValidLicense, verifyValidLicense,
@@ -13,13 +13,15 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, targetHealthCheck } from "@server/db"; import { db, targetHealthCheck, newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { addStandaloneHealthCheck } from "@server/routers/newt/targets";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -27,6 +29,7 @@ const paramsSchema = z.strictObject({
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
name: z.string().nonempty(), name: z.string().nonempty(),
siteId: z.number().int().positive(),
hcEnabled: z.boolean().default(false), hcEnabled: z.boolean().default(false),
hcMode: z.string().default("http"), hcMode: z.string().default("http"),
hcHostname: z.string().optional(), hcHostname: z.string().optional(),
@@ -97,6 +100,7 @@ export async function createHealthCheck(
const { const {
name, name,
siteId,
hcEnabled, hcEnabled,
hcMode, hcMode,
hcHostname, hcHostname,
@@ -120,6 +124,7 @@ export async function createHealthCheck(
.values({ .values({
targetId: null, targetId: null,
orgId, orgId,
siteId,
name, name,
hcEnabled, hcEnabled,
hcMode, hcMode,
@@ -140,6 +145,31 @@ export async function createHealthCheck(
}) })
.returning(); .returning();
// Push health check to newt if the site is a newt site
if (siteId) {
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (site && site.type === "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (newt) {
await addStandaloneHealthCheck(
newt.newtId,
record,
newt.version
);
}
}
}
return response<CreateHealthCheckResponse>(res, { return response<CreateHealthCheckResponse>(res, {
data: { data: {
targetHealthCheckId: record.targetHealthCheckId targetHealthCheckId: record.targetHealthCheckId
@@ -13,7 +13,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, targetHealthCheck } from "@server/db"; import { db, targetHealthCheck, newts, sites } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -21,6 +21,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { removeStandaloneHealthCheck } from "@server/routers/newt/targets";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -91,6 +92,21 @@ export async function deleteHealthCheck(
) )
); );
// Remove health check from newt if the site is a newt site
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, existing.siteId))
.limit(1);
if (newt) {
await removeStandaloneHealthCheck(
newt.newtId,
healthCheckId,
newt.version
);
}
return response<null>(res, { return response<null>(res, {
data: null, data: null,
success: true, success: true,
@@ -43,7 +43,7 @@ export async function getHealthCheckStatusHistory(
} }
const entityType = "healthCheck"; const entityType = "healthCheck";
const entityId = parsedParams.data.healthCheckId const entityId = parsedParams.data.healthCheckId;
const { days } = parsedQuery.data; const { days } = parsedQuery.data;
const nowSec = Math.floor(Date.now() / 1000); const nowSec = Math.floor(Date.now() / 1000);
@@ -11,13 +11,13 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { db, targetHealthCheck, targets, resources } from "@server/db"; import { db, targetHealthCheck, targets, resources, sites } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, isNull, sql } from "drizzle-orm"; import { and, eq, like, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
@@ -39,7 +39,8 @@ const querySchema = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.int().nonnegative()) .pipe(z.int().nonnegative()),
query: z.string().optional()
}); });
registry.registerPath({ registry.registerPath({
@@ -80,16 +81,25 @@ export async function listHealthChecks(
) )
); );
} }
const { limit, offset } = parsedQuery.data; const { limit, offset, query } = parsedQuery.data;
const whereClause = and( const whereClause = and(
eq(targetHealthCheck.orgId, orgId), eq(targetHealthCheck.orgId, orgId),
query
? like(
sql`LOWER(${targetHealthCheck.name})`,
`%${query.toLowerCase()}%`
)
: undefined
); );
const list = await db const list = await db
.select({ .select({
targetHealthCheckId: targetHealthCheck.targetHealthCheckId, targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
name: targetHealthCheck.name, name: targetHealthCheck.name,
siteId: targetHealthCheck.siteId,
siteName: sites.name,
siteNiceId: sites.niceId,
hcEnabled: targetHealthCheck.hcEnabled, hcEnabled: targetHealthCheck.hcEnabled,
hcHealth: targetHealthCheck.hcHealth, hcHealth: targetHealthCheck.hcHealth,
hcMode: targetHealthCheck.hcMode, hcMode: targetHealthCheck.hcMode,
@@ -114,6 +124,7 @@ export async function listHealthChecks(
.from(targetHealthCheck) .from(targetHealthCheck)
.leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId)) .leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId))
.leftJoin(resources, eq(targets.resourceId, resources.resourceId)) .leftJoin(resources, eq(targets.resourceId, resources.resourceId))
.leftJoin(sites, eq(targetHealthCheck.siteId, sites.siteId))
.where(whereClause) .where(whereClause)
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`) .orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
.limit(limit) .limit(limit)
@@ -129,6 +140,9 @@ export async function listHealthChecks(
healthChecks: list.map((row) => ({ healthChecks: list.map((row) => ({
targetHealthCheckId: row.targetHealthCheckId, targetHealthCheckId: row.targetHealthCheckId,
name: row.name ?? "", name: row.name ?? "",
siteId: row.siteId ?? null,
siteName: row.siteName ?? null,
siteNiceId: row.siteNiceId ?? null,
hcEnabled: row.hcEnabled, hcEnabled: row.hcEnabled,
hcHealth: (row.hcHealth ?? "unknown") as hcHealth: (row.hcHealth ?? "unknown") as
| "unknown" | "unknown"
@@ -13,7 +13,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, targetHealthCheck } from "@server/db"; import { db, targetHealthCheck, newts, sites } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -21,6 +21,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { addStandaloneHealthCheck } from "@server/routers/newt/targets";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -34,6 +35,7 @@ const paramsSchema = z
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
name: z.string().nonempty().optional(), name: z.string().nonempty().optional(),
siteId: z.number().int().positive().optional(),
hcEnabled: z.boolean().optional(), hcEnabled: z.boolean().optional(),
hcMode: z.string().optional(), hcMode: z.string().optional(),
hcHostname: z.string().optional(), hcHostname: z.string().optional(),
@@ -55,6 +57,7 @@ const bodySchema = z.strictObject({
export type UpdateHealthCheckResponse = { export type UpdateHealthCheckResponse = {
targetHealthCheckId: number; targetHealthCheckId: number;
name: string | null; name: string | null;
siteId: number | null;
hcEnabled: boolean; hcEnabled: boolean;
hcHealth: string | null; hcHealth: string | null;
hcMode: string | null; hcMode: string | null;
@@ -125,10 +128,7 @@ export async function updateHealthCheck(
.from(targetHealthCheck) .from(targetHealthCheck)
.where( .where(
and( and(
eq( eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
targetHealthCheck.targetHealthCheckId,
healthCheckId
),
eq(targetHealthCheck.orgId, orgId), eq(targetHealthCheck.orgId, orgId),
isNull(targetHealthCheck.targetId) isNull(targetHealthCheck.targetId)
) )
@@ -145,6 +145,7 @@ export async function updateHealthCheck(
const { const {
name, name,
siteId,
hcEnabled, hcEnabled,
hcMode, hcMode,
hcHostname, hcHostname,
@@ -166,6 +167,7 @@ export async function updateHealthCheck(
const updateData: Record<string, unknown> = {}; const updateData: Record<string, unknown> = {};
if (name !== undefined) updateData.name = name; if (name !== undefined) updateData.name = name;
if (siteId !== undefined) updateData.siteId = siteId;
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled; if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
if (hcMode !== undefined) updateData.hcMode = hcMode; if (hcMode !== undefined) updateData.hcMode = hcMode;
if (hcHostname !== undefined) updateData.hcHostname = hcHostname; if (hcHostname !== undefined) updateData.hcHostname = hcHostname;
@@ -193,19 +195,28 @@ export async function updateHealthCheck(
.set(updateData) .set(updateData)
.where( .where(
and( and(
eq( eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
targetHealthCheck.targetHealthCheckId,
healthCheckId
),
eq(targetHealthCheck.orgId, orgId), eq(targetHealthCheck.orgId, orgId),
isNull(targetHealthCheck.targetId) isNull(targetHealthCheck.targetId)
) )
) )
.returning(); .returning();
// Push updated health check to newt if the site is a newt site
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, updated.siteId))
.limit(1);
if (newt) {
await addStandaloneHealthCheck(newt.newtId, updated, newt.version);
}
return response<UpdateHealthCheckResponse>(res, { return response<UpdateHealthCheckResponse>(res, {
data: { data: {
targetHealthCheckId: updated.targetHealthCheckId, targetHealthCheckId: updated.targetHealthCheckId,
siteId: updated.siteId ?? null,
name: updated.name ?? null, name: updated.name ?? null,
hcEnabled: updated.hcEnabled, hcEnabled: updated.hcEnabled,
hcHealth: updated.hcHealth ?? null, hcHealth: updated.hcHealth ?? null,
+22
View File
@@ -14,6 +14,7 @@
import * as orgIdp from "#private/routers/orgIdp"; import * as orgIdp from "#private/routers/orgIdp";
import * as org from "#private/routers/org"; import * as org from "#private/routers/org";
import * as logs from "#private/routers/auditLogs"; import * as logs from "#private/routers/auditLogs";
import * as alertEvents from "#private/routers/alertEvents";
import { import {
verifyApiKeyHasAction, verifyApiKeyHasAction,
@@ -40,6 +41,27 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
export const unauthenticated = ua; export const unauthenticated = ua;
export const authenticated = a; export const authenticated = a;
authenticated.post(
"/org/:orgId/site/:siteId/trigger-alert",
verifyApiKeyIsRoot,
verifyApiKeyHasAction(ActionsEnum.triggerSiteAlert),
alertEvents.triggerSiteAlert
);
authenticated.post(
"/org/:orgId/resource/:resourceId/trigger-alert",
verifyApiKeyIsRoot,
verifyApiKeyHasAction(ActionsEnum.triggerResourceAlert),
alertEvents.triggerResourceAlert
);
authenticated.post(
"/org/:orgId/health-check/:healthCheckId/trigger-alert",
verifyApiKeyIsRoot,
verifyApiKeyHasAction(ActionsEnum.triggerHealthCheckAlert),
alertEvents.triggerHealthCheckAlert
);
authenticated.post( authenticated.post(
`/org/:orgId/send-usage-notification`, `/org/:orgId/send-usage-notification`,
verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine
@@ -27,7 +27,6 @@ import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
import { build } from "@server/build"; import { build } from "@server/build";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -45,6 +44,7 @@ const bodySchema = z.strictObject({
autoProvision: z.boolean().optional(), autoProvision: z.boolean().optional(),
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"), variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"),
roleMapping: z.string().optional(), roleMapping: z.string().optional(),
orgMapping: z.string().nullish(),
tags: z.string().optional() 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 { const {
clientId, clientId,
clientSecret, clientSecret,
@@ -118,6 +106,7 @@ export async function createOrgOidcIdp(
name, name,
variant, variant,
roleMapping, roleMapping,
orgMapping: orgMappingBody,
tags tags
} = parsedBody.data; } = parsedBody.data;
@@ -169,7 +158,7 @@ export async function createOrgOidcIdp(
idpId: idpRes.idpId, idpId: idpRes.idpId,
orgId: orgId, orgId: orgId,
roleMapping: roleMapping || null, roleMapping: roleMapping || null,
orgMapping: `'${orgId}'` orgMapping: orgMappingBody
}); });
}); });
@@ -22,7 +22,6 @@ import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { idp, idpOidcConfig, idpOrg } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -60,18 +59,6 @@ export async function deleteOrgIdp(
const { idpId } = parsedParams.data; 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 // Check if IDP exists
const [existingIdp] = await db const [existingIdp] = await db
.select() .select()
@@ -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<number>().int().positive()
});
const bodySchema = z.strictObject({
sourceOrgId: z.string().nonempty()
});
async function userIsOrgAdmin(
userId: string,
orgId: string,
session: Request["session"]
): Promise<boolean> {
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<any> {
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<CreateOrgIdpResponse>(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")
);
}
}

Some files were not shown because too many files have changed in this diff Show More