mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-12 05:36:38 +00:00
Compare commits
144 Commits
1.16.0-s.1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f021b73458 | ||
|
|
74f4751bcc | ||
|
|
e5bce4e180 | ||
|
|
9b0e7b381c | ||
|
|
90afe5a7ac | ||
|
|
b24de85157 | ||
|
|
eda43dffe1 | ||
|
|
82c9a1eb70 | ||
|
|
a3d4553d14 | ||
|
|
1cc5f59f66 | ||
|
|
4e2d88efdd | ||
|
|
4975cabb2c | ||
|
|
225591094f | ||
|
|
82f88f2cd3 | ||
|
|
99e6bd31b6 | ||
|
|
5c50590d7b | ||
|
|
072c89e704 | ||
|
|
dbdff6812d | ||
|
|
42b9d5158d | ||
|
|
2ba225299e | ||
|
|
cc841d5640 | ||
|
|
fa0818d3fa | ||
|
|
dec358c4cd | ||
|
|
e98f873f81 | ||
|
|
e9a2a7e752 | ||
|
|
06015d5191 | ||
|
|
af688d2a23 | ||
|
|
7d0b3ec6b5 | ||
|
|
9a0a255445 | ||
|
|
91b7ceb2cf | ||
|
|
d5a37436c0 | ||
|
|
be609b5000 | ||
|
|
d4b830b9bb | ||
|
|
14d6ff25a7 | ||
|
|
1f62f305ce | ||
|
|
a26ee4ac1a | ||
|
|
cebcf3e337 | ||
|
|
4cfcc64481 | ||
|
|
1a2069a6d9 | ||
|
|
8c6d44677d | ||
|
|
ebcef28b05 | ||
|
|
e87e12898c | ||
|
|
d60ab281cf | ||
|
|
483d54a9f0 | ||
|
|
0ab6ff9148 | ||
|
|
c87b6872e5 | ||
|
|
f315c8bc43 | ||
|
|
20fa1519fd | ||
|
|
54430afc40 | ||
|
|
7990d08fee | ||
|
|
e9042d9e2e | ||
|
|
24a15841e4 | ||
|
|
bb8f6e09fd | ||
|
|
04bc8ab694 | ||
|
|
6ac8335cf2 | ||
|
|
4c6144f8fb | ||
|
|
255003794e | ||
|
|
119d5c79a0 | ||
|
|
8e2d7c25df | ||
|
|
753dee3023 | ||
|
|
cac0272952 | ||
|
|
ee5b74f9fc | ||
|
|
1362b72cd3 | ||
|
|
35b1566962 | ||
|
|
a4bcce5a0c | ||
|
|
c03f1946e8 | ||
|
|
c11e107758 | ||
|
|
3b4e49f63a | ||
|
|
ea7253f7e8 | ||
|
|
8a529f7946 | ||
|
|
e76612e018 | ||
|
|
e1f99985d8 | ||
|
|
e0c2735635 | ||
|
|
8e6b4e243d | ||
|
|
2623fa8f02 | ||
|
|
7ff92d32cd | ||
|
|
c7f691b20a | ||
|
|
db042e520e | ||
|
|
4cab693cfc | ||
|
|
c9515ae77c | ||
|
|
d14de86f65 | ||
|
|
f6ee9db730 | ||
|
|
94353aea44 | ||
|
|
ed95f10fcc | ||
|
|
64bae5b142 | ||
|
|
19f9dda490 | ||
|
|
cdf79edb00 | ||
|
|
df53dfc936 | ||
|
|
8e2e09ab81 | ||
|
|
1eac7cbccd | ||
|
|
ddaaed65e4 | ||
|
|
8e633c21c7 | ||
|
|
e7c4ef44d8 | ||
|
|
3d71470bd2 | ||
|
|
dd627a222e | ||
|
|
62cc20fa1c | ||
|
|
0450fc9f57 | ||
|
|
c58aaf5ba6 | ||
|
|
655522d4e2 | ||
|
|
225475dcae | ||
|
|
ccb977fdfb | ||
|
|
280cbb6e22 | ||
|
|
c20babcb53 | ||
|
|
768eebe2cd | ||
|
|
44e3eedffa | ||
|
|
bb189874cb | ||
|
|
34dadd0e16 | ||
|
|
87b5cd9988 | ||
|
|
6a537a23e8 | ||
|
|
e63a6e9b77 | ||
|
|
7ce589c4f2 | ||
|
|
75a909784a | ||
|
|
244f497a9c | ||
|
|
e58f0c9f07 | ||
|
|
5f18c06e03 | ||
|
|
f36cf06e26 | ||
|
|
27d52646a0 | ||
|
|
4dd8080c55 | ||
|
|
0b35d4f2e3 | ||
|
|
54a9fb9e54 | ||
|
|
60a9e68f02 | ||
|
|
ad374298e3 | ||
|
|
c5dc4e6127 | ||
|
|
291ad831c5 | ||
|
|
0a018f0ca8 | ||
|
|
6673eeb1bb | ||
|
|
4641f0b9ef | ||
|
|
a4487964e5 | ||
|
|
fe42fdd1ec | ||
|
|
375211f184 | ||
|
|
66c377a5c9 | ||
|
|
50c2aa0111 | ||
|
|
fdeb891137 | ||
|
|
6a6e3a43b1 | ||
|
|
b0a34fa21b | ||
|
|
72bf6f3c41 | ||
|
|
ad9289e0c1 | ||
|
|
b0cb0e5a99 | ||
|
|
8347203bbe | ||
|
|
4aa1186aed | ||
|
|
eed87af61d | ||
|
|
daeea8e7ea | ||
|
|
81c1a1da9c | ||
|
|
52f26396ac |
@@ -1,4 +1,5 @@
|
||||
FROM node:24-slim AS base
|
||||
# FROM node:24-slim AS base
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -31,7 +32,8 @@ FROM base AS builder
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:24-slim AS runner
|
||||
# FROM node:24-slim AS runner
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ services:
|
||||
image: fosrl/pangolin:latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
|
||||
@@ -4,6 +4,12 @@ services:
|
||||
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Прокси заявки чрез HTTPS, използвайки напълно квалифицирано име на домейн.",
|
||||
"resourceRaw": "Суров TCP/UDP ресурс",
|
||||
"resourceRawDescription": "Прокси заявки чрез сурови TCP/UDP, използвайки порт номер.",
|
||||
"resourceRawDescriptionCloud": "Прокси заявките през суров TCP/UDP, използвайки номер на порт. ИЗИСКВА ИЗПОЛЗВАНЕ НА ОТДАЛЕЧЕН УЗЕЛ.",
|
||||
"resourceCreate": "Създайте ресурс",
|
||||
"resourceCreateDescription": "Следвайте стъпките по-долу, за да създадете нов ресурс",
|
||||
"resourceSeeAll": "Вижте всички ресурси",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Получаване на потребител",
|
||||
"actionGetOrgUser": "Вземете потребител на организация",
|
||||
"actionListOrgDomains": "Изброяване на домейни на организация",
|
||||
"actionGetDomain": "Вземи домейн",
|
||||
"actionCreateOrgDomain": "Създай домейн",
|
||||
"actionUpdateOrgDomain": "Актуализирай домейн",
|
||||
"actionDeleteOrgDomain": "Изтрий домейн",
|
||||
"actionGetDNSRecords": "Вземи DNS записи",
|
||||
"actionRestartOrgDomain": "Рестартирай домейн",
|
||||
"actionCreateSite": "Създаване на сайт",
|
||||
"actionDeleteSite": "Изтриване на сайта",
|
||||
"actionGetSite": "Вземете сайт",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Потребителят може да изпълнява само определени команди с sudo.",
|
||||
"sshSudo": "Разреши sudo",
|
||||
"sshSudoCommands": "Sudo команди",
|
||||
"sshSudoCommandsDescription": "Списък с команди, които потребителят е разрешено да изпълнява с sudo.",
|
||||
"sshSudoCommandsDescription": "Списък, разделен със запетаи, с команди, които потребителят е позволено да изпълнява с sudo.",
|
||||
"sshCreateHomeDir": "Създай начална директория",
|
||||
"sshUnixGroups": "Unix групи",
|
||||
"sshUnixGroupsDescription": "Unix групи, в които да добавите потребителя на целевия хост.",
|
||||
"sshUnixGroupsDescription": "Списък, разделен със запетаи, с Unix групи, към които да се добави потребителят на целевия хост.",
|
||||
"retryAttempts": "Опити за повторно",
|
||||
"expectedResponseCodes": "Очаквани кодове за отговор",
|
||||
"expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy požadavky přes HTTPS pomocí plně kvalifikovaného názvu domény.",
|
||||
"resourceRaw": "Surový TCP/UDP zdroj",
|
||||
"resourceRawDescription": "Proxy požadavky přes nezpracovaný TCP/UDP pomocí čísla portu.",
|
||||
"resourceRawDescriptionCloud": "Požadavky na proxy přes syrové TCP/UDP pomocí portového čísla. ŽÁDOSTI POUŽÍVAT POUŽITÍ Z REMOTE NODE.",
|
||||
"resourceCreate": "Vytvořit zdroj",
|
||||
"resourceCreateDescription": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili nový zdroj",
|
||||
"resourceSeeAll": "Zobrazit všechny zdroje",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Získat uživatele",
|
||||
"actionGetOrgUser": "Získat uživatele organizace",
|
||||
"actionListOrgDomains": "Seznam domén organizace",
|
||||
"actionGetDomain": "Získat doménu",
|
||||
"actionCreateOrgDomain": "Vytvořit doménu",
|
||||
"actionUpdateOrgDomain": "Aktualizovat doménu",
|
||||
"actionDeleteOrgDomain": "Odstranit doménu",
|
||||
"actionGetDNSRecords": "Získat záznamy DNS",
|
||||
"actionRestartOrgDomain": "Restartovat doménu",
|
||||
"actionCreateSite": "Vytvořit lokalitu",
|
||||
"actionDeleteSite": "Odstranění lokality",
|
||||
"actionGetSite": "Získat web",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Uživatel může spustit pouze zadané příkazy s sudo.",
|
||||
"sshSudo": "Povolit sudo",
|
||||
"sshSudoCommands": "Sudo příkazy",
|
||||
"sshSudoCommandsDescription": "Seznam příkazů, které může uživatel spouštět s sudo.",
|
||||
"sshSudoCommandsDescription": "Čárkami oddělený seznam příkazů, které může uživatel spouštět s sudo.",
|
||||
"sshCreateHomeDir": "Vytvořit domovský adresář",
|
||||
"sshUnixGroups": "Unixové skupiny",
|
||||
"sshUnixGroupsDescription": "Unix skupiny přidají uživatele do cílového hostitele.",
|
||||
"sshUnixGroupsDescription": "Čárkou oddělené skupiny Unix přidají uživatele do cílového hostitele.",
|
||||
"retryAttempts": "Opakovat pokusy",
|
||||
"expectedResponseCodes": "Očekávané kódy odezvy",
|
||||
"expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy-Anfragen über HTTPS mit einem voll qualifizierten Domain-Namen.",
|
||||
"resourceRaw": "Direkte TCP/UDP Ressource (raw)",
|
||||
"resourceRawDescription": "Proxy-Anfragen über rohes TCP/UDP mit einer Portnummer.",
|
||||
"resourceRawDescriptionCloud": "Proxy-Anfragen über rohe TCP/UDP mit einer Portnummer. Erfordert die NUTZUNG eines REMOTE Knotens.",
|
||||
"resourceCreate": "Ressource erstellen",
|
||||
"resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen",
|
||||
"resourceSeeAll": "Alle Ressourcen anzeigen",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Benutzer abrufen",
|
||||
"actionGetOrgUser": "Organisationsbenutzer abrufen",
|
||||
"actionListOrgDomains": "Organisationsdomains auflisten",
|
||||
"actionGetDomain": "Domain abrufen",
|
||||
"actionCreateOrgDomain": "Domain erstellen",
|
||||
"actionUpdateOrgDomain": "Domain aktualisieren",
|
||||
"actionDeleteOrgDomain": "Domain löschen",
|
||||
"actionGetDNSRecords": "DNS-Einträge abrufen",
|
||||
"actionRestartOrgDomain": "Domain neu starten",
|
||||
"actionCreateSite": "Standort erstellen",
|
||||
"actionDeleteSite": "Standort löschen",
|
||||
"actionGetSite": "Standort abrufen",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.",
|
||||
"sshSudo": "sudo erlauben",
|
||||
"sshSudoCommands": "Sudo-Befehle",
|
||||
"sshSudoCommandsDescription": "Liste der Befehle, die der Benutzer mit sudo ausführen darf.",
|
||||
"sshSudoCommandsDescription": "Kommagetrennte Liste von Befehlen, die der Benutzer mit sudo ausführen darf.",
|
||||
"sshCreateHomeDir": "Home-Verzeichnis erstellen",
|
||||
"sshUnixGroups": "Unix-Gruppen",
|
||||
"sshUnixGroupsDescription": "Unix-Gruppen, zu denen der Benutzer auf dem Ziel-Host hinzugefügt wird.",
|
||||
"sshUnixGroupsDescription": "Durch Komma getrennte Unix-Gruppen, um den Benutzer auf dem Zielhost hinzuzufügen.",
|
||||
"retryAttempts": "Wiederholungsversuche",
|
||||
"expectedResponseCodes": "Erwartete Antwortcodes",
|
||||
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
||||
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. REQUIRES THE USE OF A REMOTE NODE.",
|
||||
"resourceCreate": "Create Resource",
|
||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||
"resourceSeeAll": "See All Resources",
|
||||
@@ -1102,6 +1103,12 @@
|
||||
"actionGetUser": "Get User",
|
||||
"actionGetOrgUser": "Get Organization User",
|
||||
"actionListOrgDomains": "List Organization Domains",
|
||||
"actionGetDomain": "Get Domain",
|
||||
"actionCreateOrgDomain": "Create Domain",
|
||||
"actionUpdateOrgDomain": "Update Domain",
|
||||
"actionDeleteOrgDomain": "Delete Domain",
|
||||
"actionGetDNSRecords": "Get DNS Records",
|
||||
"actionRestartOrgDomain": "Restart Domain",
|
||||
"actionCreateSite": "Create Site",
|
||||
"actionDeleteSite": "Delete Site",
|
||||
"actionGetSite": "Get Site",
|
||||
@@ -1670,10 +1677,10 @@
|
||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||
"sshSudo": "Allow sudo",
|
||||
"sshSudoCommands": "Sudo Commands",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.",
|
||||
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
|
||||
"sshCreateHomeDir": "Create Home Directory",
|
||||
"sshUnixGroups": "Unix Groups",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host.",
|
||||
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
||||
"retryAttempts": "Retry Attempts",
|
||||
"expectedResponseCodes": "Expected Response Codes",
|
||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||
@@ -2336,8 +2343,8 @@
|
||||
"logRetentionEndOfFollowingYear": "End of following year",
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature.",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||
"certResolver": "Certificate Resolver",
|
||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||
"selectCertResolver": "Select Certificate Resolver",
|
||||
@@ -2674,5 +2681,6 @@
|
||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||
"approvalsEmptyStateButtonText": "Manage Roles"
|
||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy proporciona solicitudes sobre HTTPS usando un nombre de dominio completamente calificado.",
|
||||
"resourceRaw": "Recurso TCP/UDP sin procesar",
|
||||
"resourceRawDescription": "Proxy proporciona solicitudes sobre TCP/UDP usando un número de puerto.",
|
||||
"resourceRawDescriptionCloud": "Las peticiones de proxy sobre TCP/UDP crudas usando un número de puerto. REQUIERE EL USO DE UN NODO REMOTE.",
|
||||
"resourceCreate": "Crear Recurso",
|
||||
"resourceCreateDescription": "Siga los siguientes pasos para crear un nuevo recurso",
|
||||
"resourceSeeAll": "Ver todos los recursos",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Obtener usuario",
|
||||
"actionGetOrgUser": "Obtener usuario de la organización",
|
||||
"actionListOrgDomains": "Listar dominios de la organización",
|
||||
"actionGetDomain": "Obtener dominio",
|
||||
"actionCreateOrgDomain": "Crear dominio",
|
||||
"actionUpdateOrgDomain": "Actualizar dominio",
|
||||
"actionDeleteOrgDomain": "Eliminar dominio",
|
||||
"actionGetDNSRecords": "Obtener registros DNS",
|
||||
"actionRestartOrgDomain": "Reiniciar dominio",
|
||||
"actionCreateSite": "Crear sitio",
|
||||
"actionDeleteSite": "Eliminar sitio",
|
||||
"actionGetSite": "Obtener sitio",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "El usuario sólo puede ejecutar los comandos especificados con sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos que el usuario puede ejecutar con sudo.",
|
||||
"sshSudoCommandsDescription": "Lista separada por comas de comandos que el usuario puede ejecutar con sudo.",
|
||||
"sshCreateHomeDir": "Crear directorio principal",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix para agregar el usuario en el host de destino.",
|
||||
"sshUnixGroupsDescription": "Grupos Unix separados por comas para agregar el usuario en el host de destino.",
|
||||
"retryAttempts": "Intentos de Reintento",
|
||||
"expectedResponseCodes": "Códigos de respuesta esperados",
|
||||
"expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy les demandes sur HTTPS en utilisant un nom de domaine entièrement qualifié.",
|
||||
"resourceRaw": "Ressource TCP/UDP brute",
|
||||
"resourceRawDescription": "Proxy les demandes sur TCP/UDP brut en utilisant un numéro de port.",
|
||||
"resourceRawDescriptionCloud": "Requêtes de proxy sur TCP/UDP brute en utilisant un numéro de port. REQUISE L'UTILISATION D'UN Nœud DE REMOTE.",
|
||||
"resourceCreate": "Créer une ressource",
|
||||
"resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource",
|
||||
"resourceSeeAll": "Voir toutes les ressources",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Obtenir l'utilisateur",
|
||||
"actionGetOrgUser": "Obtenir l'utilisateur de l'organisation",
|
||||
"actionListOrgDomains": "Lister les domaines de l'organisation",
|
||||
"actionGetDomain": "Obtenir un domaine",
|
||||
"actionCreateOrgDomain": "Créer un domaine",
|
||||
"actionUpdateOrgDomain": "Mettre à jour le domaine",
|
||||
"actionDeleteOrgDomain": "Supprimer le domaine",
|
||||
"actionGetDNSRecords": "Récupérer les enregistrements DNS",
|
||||
"actionRestartOrgDomain": "Redémarrer le domaine",
|
||||
"actionCreateSite": "Créer un site",
|
||||
"actionDeleteSite": "Supprimer un site",
|
||||
"actionGetSite": "Obtenir un site",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "L'utilisateur ne peut exécuter que les commandes spécifiées avec sudo.",
|
||||
"sshSudo": "Autoriser sudo",
|
||||
"sshSudoCommands": "Commandes Sudo",
|
||||
"sshSudoCommandsDescription": "Liste des commandes que l'utilisateur est autorisé à exécuter avec sudo.",
|
||||
"sshSudoCommandsDescription": "Liste des commandes séparées par des virgules que l'utilisateur est autorisé à exécuter avec sudo.",
|
||||
"sshCreateHomeDir": "Créer un répertoire personnel",
|
||||
"sshUnixGroups": "Groupes Unix",
|
||||
"sshUnixGroupsDescription": "Groupes Unix à ajouter à l'utilisateur sur l'hôte cible.",
|
||||
"sshUnixGroupsDescription": "Groupes Unix séparés par des virgules pour ajouter l'utilisateur sur l'hôte cible.",
|
||||
"retryAttempts": "Tentatives de réessai",
|
||||
"expectedResponseCodes": "Codes de réponse attendus",
|
||||
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
||||
"resourceRaw": "Risorsa Raw TCP/UDP",
|
||||
"resourceRawDescription": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta.",
|
||||
"resourceRawDescriptionCloud": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta. RICHIEDE L'USO DI UN NODO REMOTO.",
|
||||
"resourceCreate": "Crea Risorsa",
|
||||
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
||||
"resourceSeeAll": "Vedi Tutte Le Risorse",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Ottieni Utente",
|
||||
"actionGetOrgUser": "Ottieni Utente Organizzazione",
|
||||
"actionListOrgDomains": "Elenca Domini Organizzazione",
|
||||
"actionGetDomain": "Ottieni Dominio",
|
||||
"actionCreateOrgDomain": "Crea Dominio",
|
||||
"actionUpdateOrgDomain": "Aggiorna Dominio",
|
||||
"actionDeleteOrgDomain": "Elimina Dominio",
|
||||
"actionGetDNSRecords": "Ottieni Record DNS",
|
||||
"actionRestartOrgDomain": "Riavvia Dominio",
|
||||
"actionCreateSite": "Crea Sito",
|
||||
"actionDeleteSite": "Elimina Sito",
|
||||
"actionGetSite": "Ottieni Sito",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "L'utente può eseguire solo i comandi specificati con sudo.",
|
||||
"sshSudo": "Consenti sudo",
|
||||
"sshSudoCommands": "Comandi Sudo",
|
||||
"sshSudoCommandsDescription": "Elenco di comandi che l'utente può eseguire con sudo.",
|
||||
"sshSudoCommandsDescription": "Elenco di comandi separati da virgole che l'utente può eseguire con sudo.",
|
||||
"sshCreateHomeDir": "Crea Cartella Home",
|
||||
"sshUnixGroups": "Gruppi Unix",
|
||||
"sshUnixGroupsDescription": "Gruppi Unix su cui aggiungere l'utente sull'host di destinazione.",
|
||||
"sshUnixGroupsDescription": "Gruppi Unix separati da virgole per aggiungere l'utente sull'host di destinazione.",
|
||||
"retryAttempts": "Tentativi di Riprova",
|
||||
"expectedResponseCodes": "Codici di Risposta Attesi",
|
||||
"expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "완전한 도메인 이름을 사용해 RAW 또는 HTTPS로 프록시 요청을 수행합니다.",
|
||||
"resourceRaw": "원시 TCP/UDP 리소스",
|
||||
"resourceRawDescription": "포트 번호를 사용하여 RAW TCP/UDP로 요청을 프록시합니다.",
|
||||
"resourceRawDescriptionCloud": "원시 TCP/UDP를 포트 번호를 사용하여 프록시 요청합니다. 원격 노드 사용이 필요합니다.",
|
||||
"resourceCreate": "리소스 생성",
|
||||
"resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.",
|
||||
"resourceSeeAll": "모든 리소스 보기",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "사용자 조회",
|
||||
"actionGetOrgUser": "조직 사용자 가져오기",
|
||||
"actionListOrgDomains": "조직 도메인 목록",
|
||||
"actionGetDomain": "도메인 가져오기",
|
||||
"actionCreateOrgDomain": "도메인 생성",
|
||||
"actionUpdateOrgDomain": "도메인 업데이트",
|
||||
"actionDeleteOrgDomain": "도메인 삭제",
|
||||
"actionGetDNSRecords": "DNS 레코드 가져오기",
|
||||
"actionRestartOrgDomain": "도메인 재시작",
|
||||
"actionCreateSite": "사이트 생성",
|
||||
"actionDeleteSite": "사이트 삭제",
|
||||
"actionGetSite": "사이트 가져오기",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "사용자는 sudo로 지정된 명령만 실행할 수 있습니다.",
|
||||
"sshSudo": "Sudo 허용",
|
||||
"sshSudoCommands": "Sudo 명령",
|
||||
"sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있도록 허용된 명령 목록입니다.",
|
||||
"sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있는 명령어의 쉼표로 구분된 목록입니다.",
|
||||
"sshCreateHomeDir": "홈 디렉터리 생성",
|
||||
"sshUnixGroups": "유닉스 그룹",
|
||||
"sshUnixGroupsDescription": "대상 호스트에서 사용자를 추가할 유닉스 그룹입니다.",
|
||||
"sshUnixGroupsDescription": "대상 호스트에서 사용자에게 추가할 유닉스 그룹의 쉼표로 구분된 목록입니다.",
|
||||
"retryAttempts": "재시도 횟수",
|
||||
"expectedResponseCodes": "예상 응답 코드",
|
||||
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy forespørsler over HTTPS ved å bruke et fullstendig kvalifisert domenenavn.",
|
||||
"resourceRaw": "Rå TCP/UDP-ressurs",
|
||||
"resourceRawDescription": "Proxy forespørsler over rå TCP/UDP ved å bruke et portnummer.",
|
||||
"resourceRawDescriptionCloud": "Proxy ber om et portnummer. Om du vil bruke et sportsnummer.",
|
||||
"resourceCreate": "Opprett ressurs",
|
||||
"resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs",
|
||||
"resourceSeeAll": "Se alle ressurser",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Hent bruker",
|
||||
"actionGetOrgUser": "Hent organisasjonsbruker",
|
||||
"actionListOrgDomains": "List opp organisasjonsdomener",
|
||||
"actionGetDomain": "Få Domene",
|
||||
"actionCreateOrgDomain": "Opprett domene",
|
||||
"actionUpdateOrgDomain": "Oppdater domene",
|
||||
"actionDeleteOrgDomain": "Slett domene",
|
||||
"actionGetDNSRecords": "Hent DNS-oppføringer",
|
||||
"actionRestartOrgDomain": "Omstart Domene",
|
||||
"actionCreateSite": "Opprett område",
|
||||
"actionDeleteSite": "Slett område",
|
||||
"actionGetSite": "Hent område",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.",
|
||||
"sshSudo": "Tillat sudo",
|
||||
"sshSudoCommands": "Sudo kommandoer",
|
||||
"sshSudoCommandsDescription": "Liste av kommandoer brukeren har lov til å kjøre med sudo.",
|
||||
"sshSudoCommandsDescription": "Kommaseparert liste med kommandoer brukeren kan kjøre med sudo.",
|
||||
"sshCreateHomeDir": "Opprett hjemmappe",
|
||||
"sshUnixGroups": "Unix grupper",
|
||||
"sshUnixGroupsDescription": "Unix grupper for å legge til brukeren til målverten.",
|
||||
"sshUnixGroupsDescription": "Kommaseparerte Unix grupper for å legge brukeren til på mål-verten.",
|
||||
"retryAttempts": "Forsøk på nytt",
|
||||
"expectedResponseCodes": "Forventede svarkoder",
|
||||
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxyverzoeken via HTTPS met een volledig gekwalificeerde domeinnaam.",
|
||||
"resourceRaw": "TCP/UDP bron",
|
||||
"resourceRawDescription": "Proxyverzoeken via ruwe TCP/UDP met een poortnummer.",
|
||||
"resourceRawDescriptionCloud": "Proxy vraagt om onbewerkte TCP/UDP met behulp van een poortnummer. VEREIST HET GEBRUIK VAN EEN AFSTANDSBEDIENING NODE.",
|
||||
"resourceCreate": "Bron maken",
|
||||
"resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken",
|
||||
"resourceSeeAll": "Alle bronnen bekijken",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Gebruiker ophalen",
|
||||
"actionGetOrgUser": "Krijg organisatie-gebruiker",
|
||||
"actionListOrgDomains": "Lijst organisatie domeinen",
|
||||
"actionGetDomain": "Domein verkrijgen",
|
||||
"actionCreateOrgDomain": "Domein aanmaken",
|
||||
"actionUpdateOrgDomain": "Domein bijwerken",
|
||||
"actionDeleteOrgDomain": "Domein verwijderen",
|
||||
"actionGetDNSRecords": "Krijg DNS Records",
|
||||
"actionRestartOrgDomain": "Domein opnieuw starten",
|
||||
"actionCreateSite": "Site aanmaken",
|
||||
"actionDeleteSite": "Site verwijderen",
|
||||
"actionGetSite": "Site ophalen",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.",
|
||||
"sshSudo": "sudo toestaan",
|
||||
"sshSudoCommands": "Sudo Commando's",
|
||||
"sshSudoCommandsDescription": "Lijst van commando's die de gebruiker mag uitvoeren met een sudo.",
|
||||
"sshSudoCommandsDescription": "Komma's gescheiden lijst van commando's waar de gebruiker een sudo mee mag uitvoeren.",
|
||||
"sshCreateHomeDir": "Maak Home Directory",
|
||||
"sshUnixGroups": "Unix groepen",
|
||||
"sshUnixGroupsDescription": "Unix groepen om de gebruiker toe te voegen aan de doel host.",
|
||||
"sshUnixGroupsDescription": "Door komma's gescheiden Unix-groepen om de gebruiker toe te voegen aan de doelhost.",
|
||||
"retryAttempts": "Herhaal Pogingen",
|
||||
"expectedResponseCodes": "Verwachte Reactiecodes",
|
||||
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxy zapytań przez HTTPS przy użyciu w pełni kwalifikowanej nazwy domeny.",
|
||||
"resourceRaw": "Surowy zasób TCP/UDP",
|
||||
"resourceRawDescription": "Proxy zapytań przez surowe TCP/UDP przy użyciu numeru portu.",
|
||||
"resourceRawDescriptionCloud": "Proxy żądania przesyłania danych nad surowym TCP/UDP przy użyciu numeru portu. Wymaga UŻYTKOWANIA PALIWA węzła.",
|
||||
"resourceCreate": "Utwórz zasób",
|
||||
"resourceCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowy zasób",
|
||||
"resourceSeeAll": "Zobacz wszystkie zasoby",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Pobierz użytkownika",
|
||||
"actionGetOrgUser": "Pobierz użytkownika organizacji",
|
||||
"actionListOrgDomains": "Lista domen organizacji",
|
||||
"actionGetDomain": "Pobierz domenę",
|
||||
"actionCreateOrgDomain": "Utwórz domenę",
|
||||
"actionUpdateOrgDomain": "Aktualizuj domenę",
|
||||
"actionDeleteOrgDomain": "Usuń domenę",
|
||||
"actionGetDNSRecords": "Pobierz rekordy DNS",
|
||||
"actionRestartOrgDomain": "Zrestartuj domenę",
|
||||
"actionCreateSite": "Utwórz witrynę",
|
||||
"actionDeleteSite": "Usuń witrynę",
|
||||
"actionGetSite": "Pobierz witrynę",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Użytkownik może uruchamiać tylko określone polecenia z sudo.",
|
||||
"sshSudo": "Zezwól na sudo",
|
||||
"sshSudoCommands": "Komendy Sudo",
|
||||
"sshSudoCommandsDescription": "Lista poleceń, które użytkownik może uruchamiać z sudo.",
|
||||
"sshSudoCommandsDescription": "Lista poleceń oddzielonych przecinkami, które użytkownik może uruchamiać z sudo.",
|
||||
"sshCreateHomeDir": "Utwórz katalog domowy",
|
||||
"sshUnixGroups": "Grupy Unix",
|
||||
"sshUnixGroupsDescription": "Grupy Unix do dodania użytkownika do docelowego hosta.",
|
||||
"sshUnixGroupsDescription": "Oddzielone przecinkami grupy Unix, aby dodać użytkownika do docelowego hosta.",
|
||||
"retryAttempts": "Próby Ponowienia",
|
||||
"expectedResponseCodes": "Oczekiwane Kody Odpowiedzi",
|
||||
"expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Proxies requests sobre HTTPS usando um nome de domínio totalmente qualificado.",
|
||||
"resourceRaw": "Recurso TCP/UDP bruto",
|
||||
"resourceRawDescription": "Proxies solicitações sobre TCP/UDP bruto usando um número de porta.",
|
||||
"resourceRawDescriptionCloud": "Proxy solicita sobre TCP/UDP bruto usando um número de porta. OBRIGATÓRIO O USO DE UMA NOTA REMOTA.",
|
||||
"resourceCreate": "Criar Recurso",
|
||||
"resourceCreateDescription": "Siga os passos abaixo para criar um novo recurso",
|
||||
"resourceSeeAll": "Ver todos os recursos",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Obter Usuário",
|
||||
"actionGetOrgUser": "Obter Utilizador da Organização",
|
||||
"actionListOrgDomains": "Listar Domínios da Organização",
|
||||
"actionGetDomain": "Obter domínio",
|
||||
"actionCreateOrgDomain": "Criar domínio",
|
||||
"actionUpdateOrgDomain": "Atualizar domínio",
|
||||
"actionDeleteOrgDomain": "Excluir domínio",
|
||||
"actionGetDNSRecords": "Obter registros de DNS",
|
||||
"actionRestartOrgDomain": "Reiniciar domínio",
|
||||
"actionCreateSite": "Criar Site",
|
||||
"actionDeleteSite": "Eliminar Site",
|
||||
"actionGetSite": "Obter Site",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Usuário só pode executar os comandos especificados com sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos com permissão de executar com o sudo.",
|
||||
"sshSudoCommandsDescription": "Lista separada por vírgulas de comandos que o usuário pode executar com sudo.",
|
||||
"sshCreateHomeDir": "Criar Diretório Inicial",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix para adicionar o usuário no host de destino.",
|
||||
"sshUnixGroupsDescription": "Grupos Unix separados por vírgulas para adicionar o usuário no host alvo.",
|
||||
"retryAttempts": "Tentativas de Repetição",
|
||||
"expectedResponseCodes": "Códigos de Resposta Esperados",
|
||||
"expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Проксировать запросы через HTTPS с использованием полного доменного имени.",
|
||||
"resourceRaw": "Сырой TCP/UDP-ресурс",
|
||||
"resourceRawDescription": "Проксировать запросы по сырому TCP/UDP с использованием номера порта.",
|
||||
"resourceRawDescriptionCloud": "Прокси-запросы через необработанный TCP/UDP с использованием номера порта. ТРЕБУЕТЕСЬ ИСПОЛЬЗОВАТЬ НЕОБХОДИМЫ.",
|
||||
"resourceCreate": "Создание ресурса",
|
||||
"resourceCreateDescription": "Следуйте инструкциям ниже для создания нового ресурса",
|
||||
"resourceSeeAll": "Посмотреть все ресурсы",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Получить пользователя",
|
||||
"actionGetOrgUser": "Получить пользователя организации",
|
||||
"actionListOrgDomains": "Список доменов организации",
|
||||
"actionGetDomain": "Получить домен",
|
||||
"actionCreateOrgDomain": "Создать домен",
|
||||
"actionUpdateOrgDomain": "Обновить домен",
|
||||
"actionDeleteOrgDomain": "Удалить домен",
|
||||
"actionGetDNSRecords": "Получить записи DNS",
|
||||
"actionRestartOrgDomain": "Перезапустить домен",
|
||||
"actionCreateSite": "Создать сайт",
|
||||
"actionDeleteSite": "Удалить сайт",
|
||||
"actionGetSite": "Получить сайт",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Пользователь может запускать только указанные команды с помощью sudo.",
|
||||
"sshSudo": "Разрешить sudo",
|
||||
"sshSudoCommands": "Sudo Команды",
|
||||
"sshSudoCommandsDescription": "Список команд, которые пользователю разрешено запускать с помощью sudo.",
|
||||
"sshSudoCommandsDescription": "Список команд, разделенных запятыми, которые пользователю разрешено запускать с помощью sudo.",
|
||||
"sshCreateHomeDir": "Создать домашний каталог",
|
||||
"sshUnixGroups": "Unix группы",
|
||||
"sshUnixGroupsDescription": "Unix группы для добавления пользователя на целевой хост.",
|
||||
"sshUnixGroupsDescription": "Группы Unix через запятую, чтобы добавить пользователя на целевой хост.",
|
||||
"retryAttempts": "Количество попыток повторного запроса",
|
||||
"expectedResponseCodes": "Ожидаемые коды ответов",
|
||||
"expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "Tam nitelikli bir etki alanı adı kullanarak HTTPS üzerinden proxy isteklerini yönlendirin.",
|
||||
"resourceRaw": "Ham TCP/UDP Kaynağı",
|
||||
"resourceRawDescription": "Port numarası kullanarak ham TCP/UDP üzerinden proxy isteklerini yönlendirin.",
|
||||
"resourceRawDescriptionCloud": "Bir port numarası kullanarak ham TCP/UDP üzerinden istekleri proxy ile yönlendirin. UZAKTAN BİR DÜĞÜM KULLANIMINI GEREKTİRİR.",
|
||||
"resourceCreate": "Kaynak Oluştur",
|
||||
"resourceCreateDescription": "Yeni bir kaynak oluşturmak için aşağıdaki adımları izleyin",
|
||||
"resourceSeeAll": "Tüm Kaynakları Gör",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "Kullanıcıyı Getir",
|
||||
"actionGetOrgUser": "Kuruluş Kullanıcısını Al",
|
||||
"actionListOrgDomains": "Kuruluş Alan Adlarını Listele",
|
||||
"actionGetDomain": "Alan Adını Al",
|
||||
"actionCreateOrgDomain": "Alan Adı Oluştur",
|
||||
"actionUpdateOrgDomain": "Alan Adını Güncelle",
|
||||
"actionDeleteOrgDomain": "Alan Adını Sil",
|
||||
"actionGetDNSRecords": "DNS Kayıtlarını Al",
|
||||
"actionRestartOrgDomain": "Alanı Yeniden Başlat",
|
||||
"actionCreateSite": "Site Oluştur",
|
||||
"actionDeleteSite": "Siteyi Sil",
|
||||
"actionGetSite": "Siteyi Al",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "Kullanıcı sadece belirtilen komutları sudo ile çalıştırabilir.",
|
||||
"sshSudo": "Sudo'ya izin ver",
|
||||
"sshSudoCommands": "Sudo Komutları",
|
||||
"sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların listesi.",
|
||||
"sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların virgülle ayrılmış listesi.",
|
||||
"sshCreateHomeDir": "Ev Dizini Oluştur",
|
||||
"sshUnixGroups": "Unix Grupları",
|
||||
"sshUnixGroupsDescription": "Hedef ana bilgisayarda kullanıcıya eklemek için Unix grupları.",
|
||||
"sshUnixGroupsDescription": "Hedef konakta kullanıcıya eklenecek Unix gruplarının virgülle ayrılmış listesi.",
|
||||
"retryAttempts": "Tekrar Deneme Girişimleri",
|
||||
"expectedResponseCodes": "Beklenen Yanıt Kodları",
|
||||
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
"resourceHTTPDescription": "通过使用完全限定的域名的HTTPS代理请求。",
|
||||
"resourceRaw": "TCP/UDP 资源",
|
||||
"resourceRawDescription": "通过使用端口号的原始TCP/UDP代理请求。",
|
||||
"resourceRawDescriptionCloud": "正在使用端口号的 TCP/UDP 代理请求。请使用一个REMOTE",
|
||||
"resourceCreate": "创建资源",
|
||||
"resourceCreateDescription": "按照下面的步骤创建新资源",
|
||||
"resourceSeeAll": "查看所有资源",
|
||||
@@ -1101,6 +1102,12 @@
|
||||
"actionGetUser": "获取用户",
|
||||
"actionGetOrgUser": "获取组织用户",
|
||||
"actionListOrgDomains": "列出组织域",
|
||||
"actionGetDomain": "获取域",
|
||||
"actionCreateOrgDomain": "创建域",
|
||||
"actionUpdateOrgDomain": "更新域",
|
||||
"actionDeleteOrgDomain": "删除域",
|
||||
"actionGetDNSRecords": "获取 DNS 记录",
|
||||
"actionRestartOrgDomain": "重新启动域",
|
||||
"actionCreateSite": "创建站点",
|
||||
"actionDeleteSite": "删除站点",
|
||||
"actionGetSite": "获取站点",
|
||||
@@ -1669,10 +1676,10 @@
|
||||
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
|
||||
"sshSudo": "允许Sudo",
|
||||
"sshSudoCommands": "Sudo 命令",
|
||||
"sshSudoCommandsDescription": "允许用户使用 sudo 运行的命令列表。",
|
||||
"sshSudoCommandsDescription": "逗号分隔的用户允许使用 sudo 运行的命令列表。",
|
||||
"sshCreateHomeDir": "创建主目录",
|
||||
"sshUnixGroups": "Unix 组",
|
||||
"sshUnixGroupsDescription": "将用户添加到目标主机的Unix组。",
|
||||
"sshUnixGroupsDescription": "用逗号分隔了Unix组,将用户添加到目标主机上。",
|
||||
"retryAttempts": "重试次数",
|
||||
"expectedResponseCodes": "期望响应代码",
|
||||
"expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。",
|
||||
|
||||
3938
package-lock.json
generated
3938
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||
"@aws-sdk/client-s3": "3.989.0",
|
||||
"@aws-sdk/client-s3": "3.1004.0",
|
||||
"@faker-js/faker": "10.3.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
@@ -80,16 +80,16 @@
|
||||
"d3": "7.9.0",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-rate-limit": "8.3.0",
|
||||
"glob": "13.0.6",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.9.3",
|
||||
"ioredis": "5.10.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.563.0",
|
||||
"lucide-react": "0.577.0",
|
||||
"maxmind": "5.0.5",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.12",
|
||||
@@ -99,21 +99,21 @@
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.1",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.19.0",
|
||||
"posthog-node": "5.26.0",
|
||||
"pg": "8.20.0",
|
||||
"posthog-node": "5.28.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.13.2",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-icons": "5.5.0",
|
||||
"react-icons": "5.6.0",
|
||||
"recharts": "2.15.4",
|
||||
"reodotdev": "1.0.0",
|
||||
"reodotdev": "1.1.0",
|
||||
"resend": "6.9.2",
|
||||
"semver": "7.7.4",
|
||||
"sshpk": "^1.18.0",
|
||||
"stripe": "20.3.1",
|
||||
"stripe": "20.4.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"topojson-client": "3.1.0",
|
||||
@@ -131,10 +131,10 @@
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.52.0",
|
||||
"@dotenvx/dotenvx": "1.54.1",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/preview-server": "5.2.8",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
@@ -146,10 +146,10 @@
|
||||
"@types/jmespath": "0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.2.3",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/nodemailer": "7.0.11",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.16.0",
|
||||
"@types/pg": "8.18.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
@@ -167,10 +167,14 @@
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"react-email": "5.2.8",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tailwindcss": "4.2.1",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.55.0"
|
||||
"typescript-eslint": "8.56.1"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.27.3",
|
||||
"dompurify": "3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,14 @@ export const approvals = pgTable("approvals", {
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const bannedEmails = pgTable("bannedEmails", {
|
||||
email: varchar("email", { length: 255 }).primaryKey(),
|
||||
});
|
||||
|
||||
export const bannedIps = pgTable("bannedIps", {
|
||||
ip: varchar("ip", { length: 255 }).primaryKey(),
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
|
||||
@@ -22,7 +22,8 @@ export const domains = pgTable("domains", {
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: varchar("certResolver"),
|
||||
customCertResolver: varchar("customCertResolver"),
|
||||
preferWildcardCert: boolean("preferWildcardCert")
|
||||
preferWildcardCert: boolean("preferWildcardCert"),
|
||||
errorMessage: text("errorMessage")
|
||||
});
|
||||
|
||||
export const dnsRecords = pgTable("dnsRecords", {
|
||||
@@ -283,6 +284,7 @@ export const users = pgTable("user", {
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
||||
termsVersion: varchar("termsVersion"),
|
||||
marketingEmailConsent: boolean("marketingEmailConsent").default(false),
|
||||
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
||||
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" })
|
||||
});
|
||||
|
||||
@@ -318,6 +318,15 @@ export const approvals = sqliteTable("approvals", {
|
||||
.notNull()
|
||||
});
|
||||
|
||||
|
||||
export const bannedEmails = sqliteTable("bannedEmails", {
|
||||
email: text("email").primaryKey()
|
||||
});
|
||||
|
||||
export const bannedIps = sqliteTable("bannedIps", {
|
||||
ip: text("ip").primaryKey()
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
|
||||
@@ -13,7 +13,8 @@ export const domains = sqliteTable("domains", {
|
||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: text("certResolver"),
|
||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
|
||||
errorMessage: text("errorMessage")
|
||||
});
|
||||
|
||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||
@@ -314,6 +315,9 @@ export const users = sqliteTable("user", {
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
||||
termsVersion: text("termsVersion"),
|
||||
marketingEmailConsent: integer("marketingEmailConsent", {
|
||||
mode: "boolean"
|
||||
}).default(false),
|
||||
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
@@ -17,6 +17,7 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import { APP_PATH } from "./lib/consts";
|
||||
import yaml from "js-yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
const dev = process.env.ENVIRONMENT !== "prod";
|
||||
const externalPort = config.getRawConfig().server.integration_port;
|
||||
@@ -38,12 +39,24 @@ export function createIntegrationApiServer() {
|
||||
apiServer.use(cookieParser());
|
||||
apiServer.use(express.json());
|
||||
|
||||
const openApiDocumentation = getOpenApiDocumentation();
|
||||
|
||||
apiServer.use(
|
||||
"/v1/docs",
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(getOpenApiDocumentation())
|
||||
swaggerUi.setup(openApiDocumentation)
|
||||
);
|
||||
|
||||
// Unauthenticated OpenAPI spec endpoints
|
||||
apiServer.get("/v1/openapi.json", (_req, res) => {
|
||||
res.json(openApiDocumentation);
|
||||
});
|
||||
|
||||
apiServer.get("/v1/openapi.yaml", (_req, res) => {
|
||||
const yamlOutput = yaml.dump(openApiDocumentation);
|
||||
res.type("application/yaml").send(yamlOutput);
|
||||
});
|
||||
|
||||
// API routes
|
||||
const prefix = `/v1`;
|
||||
apiServer.use(logIncomingMiddleware);
|
||||
@@ -75,16 +88,6 @@ function getOpenApiDocumentation() {
|
||||
}
|
||||
);
|
||||
|
||||
for (const def of registry.definitions) {
|
||||
if (def.type === "route") {
|
||||
def.route.security = [
|
||||
{
|
||||
[bearerAuth.name]: []
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/",
|
||||
@@ -94,6 +97,74 @@ function getOpenApiDocumentation() {
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/openapi.json",
|
||||
description: "Get OpenAPI specification as JSON",
|
||||
tags: [],
|
||||
request: {},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "OpenAPI specification as JSON",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/openapi.yaml",
|
||||
description: "Get OpenAPI specification as YAML",
|
||||
tags: [],
|
||||
request: {},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "OpenAPI specification as YAML",
|
||||
content: {
|
||||
"application/yaml": {
|
||||
schema: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const def of registry.definitions) {
|
||||
if (def.type === "route") {
|
||||
def.route.security = [
|
||||
{
|
||||
[bearerAuth.name]: []
|
||||
}
|
||||
];
|
||||
|
||||
// Ensure every route has a generic JSON response schema so Swagger UI can render responses
|
||||
const existingResponses = def.route.responses;
|
||||
const hasExistingResponses =
|
||||
existingResponses && Object.keys(existingResponses).length > 0;
|
||||
|
||||
if (!hasExistingResponses) {
|
||||
def.route.responses = {
|
||||
"*": {
|
||||
description: "",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||
|
||||
const generated = generator.generateDocument({
|
||||
|
||||
@@ -85,9 +85,7 @@ export async function deleteOrgById(
|
||||
deletedNewtIds.push(deletedNewt.newtId);
|
||||
await trx
|
||||
.delete(newtSessions)
|
||||
.where(
|
||||
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||
);
|
||||
.where(eq(newtSessions.newtId, deletedNewt.newtId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,33 +119,38 @@ export async function deleteOrgById(
|
||||
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
||||
);
|
||||
}
|
||||
|
||||
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
||||
|
||||
const allOrgDomains = await trx
|
||||
.select()
|
||||
.from(orgDomains)
|
||||
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
|
||||
.innerJoin(domains, eq(orgDomains.domainId, domains.domainId))
|
||||
.where(
|
||||
and(
|
||||
eq(orgDomains.orgId, orgId),
|
||||
eq(domains.configManaged, false)
|
||||
)
|
||||
);
|
||||
logger.info(`Found ${allOrgDomains.length} domains to delete`);
|
||||
const domainIdsToDelete: string[] = [];
|
||||
for (const orgDomain of allOrgDomains) {
|
||||
const domainId = orgDomain.domains.domainId;
|
||||
const orgCount = await trx
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
const [orgCount] = await trx
|
||||
.select({ count: count() })
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.domainId, domainId));
|
||||
if (orgCount[0].count === 1) {
|
||||
logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`);
|
||||
if (orgCount.count === 1) {
|
||||
domainIdsToDelete.push(domainId);
|
||||
}
|
||||
}
|
||||
logger.info(`Found ${domainIdsToDelete.length} domains to delete`);
|
||||
if (domainIdsToDelete.length > 0) {
|
||||
await trx
|
||||
.delete(domains)
|
||||
.where(inArray(domains.domainId, domainIdsToDelete));
|
||||
}
|
||||
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
||||
|
||||
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
|
||||
|
||||
@@ -231,15 +234,13 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
|
||||
);
|
||||
}
|
||||
for (const olmId of result.olmsToTerminate) {
|
||||
sendTerminateClient(
|
||||
0,
|
||||
OlmErrorCodes.TERMINATED_REKEYED,
|
||||
olmId
|
||||
).catch((error) => {
|
||||
logger.error(
|
||||
"Failed to send termination message to olm:",
|
||||
error
|
||||
);
|
||||
});
|
||||
sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch(
|
||||
(error) => {
|
||||
logger.error(
|
||||
"Failed to send termination message to olm:",
|
||||
error
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +571,7 @@ export async function updateClientSiteDestinations(
|
||||
destinations: [
|
||||
{
|
||||
destinationIP: site.sites.subnet.split("/")[0],
|
||||
destinationPort: site.sites.listenPort || 0
|
||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -579,7 +579,7 @@ export async function updateClientSiteDestinations(
|
||||
// add to the existing destinations
|
||||
destinations.destinations.push({
|
||||
destinationIP: site.sites.subnet.split("/")[0],
|
||||
destinationPort: site.sites.listenPort || 0
|
||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export enum AudienceIds {
|
||||
SignUps = "",
|
||||
Subscribed = "",
|
||||
Churned = "",
|
||||
Newsletter = ""
|
||||
}
|
||||
|
||||
let resend;
|
||||
export default resend;
|
||||
|
||||
export async function moveEmailToAudience(
|
||||
email: string,
|
||||
audienceId: AudienceIds
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -218,10 +218,11 @@ export class TraefikConfigManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch if it's been more than 24 hours (for renewals)
|
||||
const dayInMs = 24 * 60 * 60 * 1000;
|
||||
const timeSinceLastFetch =
|
||||
Date.now() - this.lastCertificateFetch.getTime();
|
||||
|
||||
// Fetch if it's been more than 24 hours (daily routine check)
|
||||
if (timeSinceLastFetch > dayInMs) {
|
||||
logger.info("Fetching certificates due to 24-hour renewal check");
|
||||
return true;
|
||||
@@ -265,7 +266,7 @@ export class TraefikConfigManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any local certificates are missing or appear to be outdated
|
||||
// Check if any local certificates are missing (needs immediate fetch)
|
||||
for (const domain of domainsNeedingCerts) {
|
||||
const localState = this.lastLocalCertificateState.get(domain);
|
||||
if (!localState || !localState.exists) {
|
||||
@@ -274,17 +275,55 @@ export class TraefikConfigManager {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if certificate is expiring soon (within 30 days)
|
||||
if (localState.expiresAt) {
|
||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||
const secondsUntilExpiry = localState.expiresAt - nowInSeconds;
|
||||
const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24);
|
||||
if (daysUntilExpiry < 30) {
|
||||
logger.info(
|
||||
`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`
|
||||
);
|
||||
return true;
|
||||
// For expiry checks, throttle to every 6 hours to avoid querying the
|
||||
// API/DB on every monitor loop. The certificate-service renews certs
|
||||
// 45 days before expiry, so checking every 6 hours is plenty frequent
|
||||
// to pick up renewed certs promptly.
|
||||
const renewalCheckIntervalMs = 6 * 60 * 60 * 1000; // 6 hours
|
||||
if (timeSinceLastFetch > renewalCheckIntervalMs) {
|
||||
// Check non-wildcard certs for expiry (within 45 days to match
|
||||
// the server-side renewal window in certificate-service)
|
||||
for (const domain of domainsNeedingCerts) {
|
||||
const localState =
|
||||
this.lastLocalCertificateState.get(domain);
|
||||
if (localState?.expiresAt) {
|
||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||
const secondsUntilExpiry =
|
||||
localState.expiresAt - nowInSeconds;
|
||||
const daysUntilExpiry =
|
||||
secondsUntilExpiry / (60 * 60 * 24);
|
||||
if (daysUntilExpiry < 45) {
|
||||
logger.info(
|
||||
`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check wildcard certificates for expiry. These are not
|
||||
// included in domainsNeedingCerts since their subdomains are
|
||||
// filtered out, so we must check them separately.
|
||||
for (const [certDomain, state] of this
|
||||
.lastLocalCertificateState) {
|
||||
if (
|
||||
state.exists &&
|
||||
state.wildcard &&
|
||||
state.expiresAt
|
||||
) {
|
||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||
const secondsUntilExpiry =
|
||||
state.expiresAt - nowInSeconds;
|
||||
const daysUntilExpiry =
|
||||
secondsUntilExpiry / (60 * 60 * 24);
|
||||
if (daysUntilExpiry < 45) {
|
||||
logger.info(
|
||||
`Fetching certificates due to upcoming expiry for wildcard cert ${certDomain} (${Math.round(daysUntilExpiry)} days remaining)`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +400,32 @@ export class TraefikConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Also include wildcard cert base domains that are
|
||||
// expiring or expired so they get re-fetched even though
|
||||
// their subdomains were filtered out above.
|
||||
for (const [certDomain, state] of this
|
||||
.lastLocalCertificateState) {
|
||||
if (
|
||||
state.exists &&
|
||||
state.wildcard &&
|
||||
state.expiresAt
|
||||
) {
|
||||
const nowInSeconds = Math.floor(
|
||||
Date.now() / 1000
|
||||
);
|
||||
const secondsUntilExpiry =
|
||||
state.expiresAt - nowInSeconds;
|
||||
const daysUntilExpiry =
|
||||
secondsUntilExpiry / (60 * 60 * 24);
|
||||
if (daysUntilExpiry < 45) {
|
||||
domainsToFetch.add(certDomain);
|
||||
logger.info(
|
||||
`Including expiring wildcard cert domain ${certDomain} in fetch (${Math.round(daysUntilExpiry)} days remaining)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (domainsToFetch.size > 0) {
|
||||
// Get valid certificates for domains not covered by wildcards
|
||||
validCertificates =
|
||||
|
||||
@@ -14,7 +14,7 @@ import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { resources, sites, Target, targets } from "@server/db";
|
||||
import createPathRewriteMiddleware from "./middleware";
|
||||
import { sanitize, validatePathRewriteConfig } from "./utils";
|
||||
import { sanitize, encodePath, validatePathRewriteConfig } from "./utils";
|
||||
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
const badgerMiddlewareName = "badger";
|
||||
@@ -44,7 +44,7 @@ export async function getTraefikConfig(
|
||||
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
|
||||
allowMaintenancePage = true // UNUSED BUT USED IN PRIVATE
|
||||
): Promise<any> {
|
||||
// Get resources with their targets and sites in a single optimized query
|
||||
// Start from sites on this exit node, then join to targets and resources
|
||||
@@ -127,7 +127,7 @@ export async function getTraefikConfig(
|
||||
resourcesWithTargetsAndSites.forEach((row) => {
|
||||
const resourceId = row.resourceId;
|
||||
const resourceName = sanitize(row.resourceName) || "";
|
||||
const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths
|
||||
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
||||
const pathMatchType = row.pathMatchType || "";
|
||||
const rewritePath = row.rewritePath || "";
|
||||
const rewritePathType = row.rewritePathType || "";
|
||||
@@ -145,7 +145,7 @@ export async function getTraefikConfig(
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
const key = sanitize(mapKey);
|
||||
|
||||
if (!resourcesMap.has(key)) {
|
||||
if (!resourcesMap.has(mapKey)) {
|
||||
const validation = validatePathRewriteConfig(
|
||||
row.path,
|
||||
row.pathMatchType,
|
||||
@@ -160,9 +160,10 @@ export async function getTraefikConfig(
|
||||
return;
|
||||
}
|
||||
|
||||
resourcesMap.set(key, {
|
||||
resourcesMap.set(mapKey, {
|
||||
resourceId: row.resourceId,
|
||||
name: resourceName,
|
||||
key: key,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
http: row.http,
|
||||
@@ -190,7 +191,7 @@ export async function getTraefikConfig(
|
||||
});
|
||||
}
|
||||
|
||||
resourcesMap.get(key).targets.push({
|
||||
resourcesMap.get(mapKey).targets.push({
|
||||
resourceId: row.resourceId,
|
||||
targetId: row.targetId,
|
||||
ip: row.ip,
|
||||
@@ -227,8 +228,9 @@ export async function getTraefikConfig(
|
||||
};
|
||||
|
||||
// get the key and the resource
|
||||
for (const [key, resource] of resourcesMap.entries()) {
|
||||
for (const [, resource] of resourcesMap.entries()) {
|
||||
const targets = resource.targets as TargetWithSite[];
|
||||
const key = resource.key;
|
||||
|
||||
const routerName = `${key}-${resource.name}-router`;
|
||||
const serviceName = `${key}-${resource.name}-service`;
|
||||
@@ -477,7 +479,10 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -490,7 +495,7 @@ export async function getTraefikConfig(
|
||||
if (target.health == "unhealthy") {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If any sites are online, exclude offline sites
|
||||
if (anySitesOnline && !target.site.online) {
|
||||
return false;
|
||||
@@ -605,7 +610,10 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return targets
|
||||
@@ -613,7 +621,7 @@ export async function getTraefikConfig(
|
||||
if (!target.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If any sites are online, exclude offline sites
|
||||
if (anySitesOnline && !target.site.online) {
|
||||
return false;
|
||||
|
||||
323
server/lib/traefik/pathEncoding.test.ts
Normal file
323
server/lib/traefik/pathEncoding.test.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import { assertEquals } from "../../../test/assert";
|
||||
|
||||
// ── Pure function copies (inlined to avoid pulling in server dependencies) ──
|
||||
|
||||
function sanitize(input: string | null | undefined): string | undefined {
|
||||
if (!input) return undefined;
|
||||
if (input.length > 50) {
|
||||
input = input.substring(0, 50);
|
||||
}
|
||||
return input
|
||||
.replace(/[^a-zA-Z0-9-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function encodePath(path: string | null | undefined): string {
|
||||
if (!path) return "";
|
||||
return path.replace(/[^a-zA-Z0-9]/g, (ch) => {
|
||||
return ch.charCodeAt(0).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Exact replica of the OLD key computation from upstream main.
|
||||
* Uses sanitize() for paths — this is what had the collision bug.
|
||||
*/
|
||||
function oldKeyComputation(
|
||||
resourceId: number,
|
||||
path: string | null,
|
||||
pathMatchType: string | null,
|
||||
rewritePath: string | null,
|
||||
rewritePathType: string | null
|
||||
): string {
|
||||
const targetPath = sanitize(path) || "";
|
||||
const pmt = pathMatchType || "";
|
||||
const rp = rewritePath || "";
|
||||
const rpt = rewritePathType || "";
|
||||
const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-");
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
return sanitize(mapKey) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Replica of the NEW key computation from our fix.
|
||||
* Uses encodePath() for paths — collision-free.
|
||||
*/
|
||||
function newKeyComputation(
|
||||
resourceId: number,
|
||||
path: string | null,
|
||||
pathMatchType: string | null,
|
||||
rewritePath: string | null,
|
||||
rewritePathType: string | null
|
||||
): string {
|
||||
const targetPath = encodePath(path);
|
||||
const pmt = pathMatchType || "";
|
||||
const rp = rewritePath || "";
|
||||
const rpt = rewritePathType || "";
|
||||
const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-");
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
return sanitize(mapKey) || "";
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
function runTests() {
|
||||
console.log("Running path encoding tests...\n");
|
||||
|
||||
let passed = 0;
|
||||
|
||||
// ── encodePath unit tests ────────────────────────────────────────
|
||||
|
||||
// Test 1: null/undefined/empty
|
||||
{
|
||||
assertEquals(encodePath(null), "", "null should return empty");
|
||||
assertEquals(
|
||||
encodePath(undefined),
|
||||
"",
|
||||
"undefined should return empty"
|
||||
);
|
||||
assertEquals(encodePath(""), "", "empty string should return empty");
|
||||
console.log(" PASS: encodePath handles null/undefined/empty");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: root path
|
||||
{
|
||||
assertEquals(encodePath("/"), "2f", "/ should encode to 2f");
|
||||
console.log(" PASS: encodePath encodes root path");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: alphanumeric passthrough
|
||||
{
|
||||
assertEquals(encodePath("/api"), "2fapi", "/api encodes slash only");
|
||||
assertEquals(encodePath("/v1"), "2fv1", "/v1 encodes slash only");
|
||||
assertEquals(encodePath("abc"), "abc", "plain alpha passes through");
|
||||
console.log(" PASS: encodePath preserves alphanumeric chars");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: all special chars produce unique hex
|
||||
{
|
||||
const paths = ["/a/b", "/a-b", "/a.b", "/a_b", "/a b"];
|
||||
const results = paths.map((p) => encodePath(p));
|
||||
const unique = new Set(results);
|
||||
assertEquals(
|
||||
unique.size,
|
||||
paths.length,
|
||||
"all special-char paths must produce unique encodings"
|
||||
);
|
||||
console.log(
|
||||
" PASS: encodePath produces unique output for different special chars"
|
||||
);
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: output is always alphanumeric (safe for Traefik names)
|
||||
{
|
||||
const paths = [
|
||||
"/",
|
||||
"/api",
|
||||
"/a/b",
|
||||
"/a-b",
|
||||
"/a.b",
|
||||
"/complex/path/here"
|
||||
];
|
||||
for (const p of paths) {
|
||||
const e = encodePath(p);
|
||||
assertEquals(
|
||||
/^[a-zA-Z0-9]+$/.test(e),
|
||||
true,
|
||||
`encodePath("${p}") = "${e}" must be alphanumeric`
|
||||
);
|
||||
}
|
||||
console.log(" PASS: encodePath output is always alphanumeric");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: deterministic
|
||||
{
|
||||
assertEquals(
|
||||
encodePath("/api"),
|
||||
encodePath("/api"),
|
||||
"same input same output"
|
||||
);
|
||||
assertEquals(
|
||||
encodePath("/a/b/c"),
|
||||
encodePath("/a/b/c"),
|
||||
"same input same output"
|
||||
);
|
||||
console.log(" PASS: encodePath is deterministic");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: many distinct paths never collide
|
||||
{
|
||||
const paths = [
|
||||
"/",
|
||||
"/api",
|
||||
"/api/v1",
|
||||
"/api/v2",
|
||||
"/a/b",
|
||||
"/a-b",
|
||||
"/a.b",
|
||||
"/a_b",
|
||||
"/health",
|
||||
"/health/check",
|
||||
"/admin",
|
||||
"/admin/users",
|
||||
"/api/v1/users",
|
||||
"/api/v1/posts",
|
||||
"/app",
|
||||
"/app/dashboard"
|
||||
];
|
||||
const encoded = new Set(paths.map((p) => encodePath(p)));
|
||||
assertEquals(
|
||||
encoded.size,
|
||||
paths.length,
|
||||
`expected ${paths.length} unique encodings, got ${encoded.size}`
|
||||
);
|
||||
console.log(" PASS: 16 realistic paths all produce unique encodings");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// ── Collision fix: the actual bug we're fixing ───────────────────
|
||||
|
||||
// Test 8: /a/b and /a-b now have different keys (THE BUG FIX)
|
||||
{
|
||||
const keyAB = newKeyComputation(1, "/a/b", "prefix", null, null);
|
||||
const keyDash = newKeyComputation(1, "/a-b", "prefix", null, null);
|
||||
assertEquals(
|
||||
keyAB !== keyDash,
|
||||
true,
|
||||
"/a/b and /a-b MUST have different keys"
|
||||
);
|
||||
console.log(" PASS: collision fix — /a/b vs /a-b have different keys");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: demonstrate the old bug — old code maps /a/b and /a-b to same key
|
||||
{
|
||||
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
||||
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
||||
assertEquals(
|
||||
oldKeyAB,
|
||||
oldKeyDash,
|
||||
"old code MUST have this collision (confirms the bug exists)"
|
||||
);
|
||||
console.log(" PASS: confirmed old code bug — /a/b and /a-b collided");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: /api/v1 and /api-v1 — old code collision, new code fixes it
|
||||
{
|
||||
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||
assertEquals(
|
||||
oldKey1,
|
||||
oldKey2,
|
||||
"old code collision for /api/v1 vs /api-v1"
|
||||
);
|
||||
|
||||
const newKey1 = newKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||
const newKey2 = newKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||
assertEquals(
|
||||
newKey1 !== newKey2,
|
||||
true,
|
||||
"new code must separate /api/v1 and /api-v1"
|
||||
);
|
||||
console.log(" PASS: collision fix — /api/v1 vs /api-v1");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed
|
||||
{
|
||||
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
||||
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
||||
const c = newKeyComputation(1, "/app-v2", "prefix", null, null);
|
||||
const keys = new Set([a, b, c]);
|
||||
assertEquals(
|
||||
keys.size,
|
||||
3,
|
||||
"three paths must produce three unique keys"
|
||||
);
|
||||
console.log(
|
||||
" PASS: collision fix — three-way /app.v2, /app/v2, /app-v2"
|
||||
);
|
||||
passed++;
|
||||
}
|
||||
|
||||
// ── Edge cases ───────────────────────────────────────────────────
|
||||
|
||||
// Test 12: same path in different resources — always separate
|
||||
{
|
||||
const key1 = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const key2 = newKeyComputation(2, "/api", "prefix", null, null);
|
||||
assertEquals(
|
||||
key1 !== key2,
|
||||
true,
|
||||
"different resources with same path must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different resources");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 13: same resource, different pathMatchType — separate keys
|
||||
{
|
||||
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
||||
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
assertEquals(
|
||||
exact !== prefix,
|
||||
true,
|
||||
"exact vs prefix must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different match types");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 14: same resource and path, different rewrite config — separate keys
|
||||
{
|
||||
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const withRewrite = newKeyComputation(
|
||||
1,
|
||||
"/api",
|
||||
"prefix",
|
||||
"/backend",
|
||||
"prefix"
|
||||
);
|
||||
assertEquals(
|
||||
noRewrite !== withRewrite,
|
||||
true,
|
||||
"with vs without rewrite must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different rewrite config");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 15: paths with special URL characters
|
||||
{
|
||||
const paths = ["/api?foo", "/api#bar", "/api%20baz", "/api+qux"];
|
||||
const keys = new Set(
|
||||
paths.map((p) => newKeyComputation(1, p, "prefix", null, null))
|
||||
);
|
||||
assertEquals(
|
||||
keys.size,
|
||||
paths.length,
|
||||
"special URL chars must produce unique keys"
|
||||
);
|
||||
console.log(" PASS: edge case — special URL characters in paths");
|
||||
passed++;
|
||||
}
|
||||
|
||||
console.log(`\nAll ${passed} tests passed!`);
|
||||
}
|
||||
|
||||
try {
|
||||
runTests();
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -13,6 +13,26 @@ export function sanitize(input: string | null | undefined): string | undefined {
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a URL path into a collision-free alphanumeric string suitable for use
|
||||
* in Traefik map keys.
|
||||
*
|
||||
* Unlike sanitize(), this preserves uniqueness by encoding each non-alphanumeric
|
||||
* character as its hex code. Different paths always produce different outputs.
|
||||
*
|
||||
* encodePath("/api") => "2fapi"
|
||||
* encodePath("/a/b") => "2fa2fb"
|
||||
* encodePath("/a-b") => "2fa2db" (different from /a/b)
|
||||
* encodePath("/") => "2f"
|
||||
* encodePath(null) => ""
|
||||
*/
|
||||
export function encodePath(path: string | null | undefined): string {
|
||||
if (!path) return "";
|
||||
return path.replace(/[^a-zA-Z0-9]/g, (ch) => {
|
||||
return ch.charCodeAt(0).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export function validatePathRewriteConfig(
|
||||
path: string | null,
|
||||
pathMatchType: string | null,
|
||||
|
||||
@@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess";
|
||||
export * from "./verifyApiKeyClientAccess";
|
||||
export * from "./verifyApiKeySiteResourceAccess";
|
||||
export * from "./verifyApiKeyIdpAccess";
|
||||
export * from "./verifyApiKeyDomainAccess";
|
||||
|
||||
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db, domains, orgDomains, apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyDomainAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const domainId =
|
||||
req.params.domainId || req.body.domainId || req.query.domainId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!domainId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKey.isRoot) {
|
||||
// Root keys can access any domain in any org
|
||||
return next();
|
||||
}
|
||||
|
||||
// Verify domain exists and belongs to the organization
|
||||
const [domain] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId))
|
||||
.where(
|
||||
and(
|
||||
eq(orgDomains.domainId, domainId),
|
||||
eq(orgDomains.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!domain) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Domain with ID ${domainId} not found in organization ${orgId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the API key has access to this organization
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgRes = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying domain access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,20 @@ export const registry = new OpenAPIRegistry();
|
||||
export enum OpenAPITags {
|
||||
Site = "Site",
|
||||
Org = "Organization",
|
||||
Resource = "Resource",
|
||||
PublicResource = "Public Resource",
|
||||
PrivateResource = "Private Resource",
|
||||
Role = "Role",
|
||||
User = "User",
|
||||
Invitation = "Invitation",
|
||||
Target = "Target",
|
||||
Invitation = "User Invitation",
|
||||
Target = "Resource Target",
|
||||
Rule = "Rule",
|
||||
AccessToken = "Access Token",
|
||||
Idp = "Identity Provider",
|
||||
GlobalIdp = "Identity Provider (Global)",
|
||||
OrgIdp = "Identity Provider (Organization Only)",
|
||||
Client = "Client",
|
||||
ApiKey = "API Key",
|
||||
Domain = "Domain",
|
||||
Blueprint = "Blueprint",
|
||||
Ssh = "SSH"
|
||||
Ssh = "SSH",
|
||||
Logs = "Logs"
|
||||
}
|
||||
|
||||
@@ -38,10 +38,6 @@ export const privateConfigSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")),
|
||||
resend_api_key: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("RESEND_API_KEY")),
|
||||
reo_client_id: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 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 { Resend } from "resend";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export enum AudienceIds {
|
||||
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
|
||||
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
||||
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
||||
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
||||
}
|
||||
|
||||
const resend = new Resend(
|
||||
privateConfig.getRawPrivateConfig().server.resend_api_key || "missing"
|
||||
);
|
||||
|
||||
export default resend;
|
||||
|
||||
export async function moveEmailToAudience(
|
||||
email: string,
|
||||
audienceId: AudienceIds
|
||||
) {
|
||||
if (process.env.ENVIRONMENT !== "prod") {
|
||||
logger.debug(
|
||||
`Skipping moving email ${email} to audience ${audienceId} in non-prod environment`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { error, data } = await retryWithBackoff(async () => {
|
||||
const { data, error } = await resend.contacts.create({
|
||||
email,
|
||||
unsubscribed: false,
|
||||
audienceId
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Error adding email ${email} to audience ${audienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
return { error, data };
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Error adding email ${email} to audience ${audienceId}: ${error}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
logger.debug(
|
||||
`Added email ${email} to audience ${audienceId} with contact ID ${data.id}`
|
||||
);
|
||||
}
|
||||
|
||||
const otherAudiences = Object.values(AudienceIds).filter(
|
||||
(id) => id !== audienceId
|
||||
);
|
||||
|
||||
for (const otherAudienceId of otherAudiences) {
|
||||
const { error, data } = await retryWithBackoff(async () => {
|
||||
const { data, error } = await resend.contacts.remove({
|
||||
email,
|
||||
audienceId: otherAudienceId
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
return { error, data };
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
logger.info(
|
||||
`Removed email ${email} from audience ${otherAudienceId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RetryOptions = {
|
||||
retries?: number;
|
||||
initialDelayMs?: number;
|
||||
factor?: number;
|
||||
};
|
||||
|
||||
export async function retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: RetryOptions = {}
|
||||
): Promise<T> {
|
||||
const { retries = 5, initialDelayMs = 500, factor = 2 } = options;
|
||||
|
||||
let attempt = 0;
|
||||
let delay = initialDelayMs;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
attempt++;
|
||||
|
||||
if (attempt > retries) throw err;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,11 @@ import {
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||
import { sanitize, validatePathRewriteConfig } from "@server/lib/traefik/utils";
|
||||
import {
|
||||
sanitize,
|
||||
encodePath,
|
||||
validatePathRewriteConfig
|
||||
} from "@server/lib/traefik/utils";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import createPathRewriteMiddleware from "@server/lib/traefik/middleware";
|
||||
import {
|
||||
@@ -170,7 +174,7 @@ export async function getTraefikConfig(
|
||||
resourcesWithTargetsAndSites.forEach((row) => {
|
||||
const resourceId = row.resourceId;
|
||||
const resourceName = sanitize(row.resourceName) || "";
|
||||
const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths
|
||||
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
||||
const pathMatchType = row.pathMatchType || "";
|
||||
const rewritePath = row.rewritePath || "";
|
||||
const rewritePathType = row.rewritePathType || "";
|
||||
@@ -192,7 +196,7 @@ export async function getTraefikConfig(
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
const key = sanitize(mapKey);
|
||||
|
||||
if (!resourcesMap.has(key)) {
|
||||
if (!resourcesMap.has(mapKey)) {
|
||||
const validation = validatePathRewriteConfig(
|
||||
row.path,
|
||||
row.pathMatchType,
|
||||
@@ -207,9 +211,10 @@ export async function getTraefikConfig(
|
||||
return;
|
||||
}
|
||||
|
||||
resourcesMap.set(key, {
|
||||
resourcesMap.set(mapKey, {
|
||||
resourceId: row.resourceId,
|
||||
name: resourceName,
|
||||
key: key,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
http: row.http,
|
||||
@@ -243,7 +248,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
|
||||
// Add target with its associated site data
|
||||
resourcesMap.get(key).targets.push({
|
||||
resourcesMap.get(mapKey).targets.push({
|
||||
resourceId: row.resourceId,
|
||||
targetId: row.targetId,
|
||||
ip: row.ip,
|
||||
@@ -296,8 +301,9 @@ export async function getTraefikConfig(
|
||||
};
|
||||
|
||||
// get the key and the resource
|
||||
for (const [key, resource] of resourcesMap.entries()) {
|
||||
for (const [, resource] of resourcesMap.entries()) {
|
||||
const targets = resource.targets as TargetWithSite[];
|
||||
const key = resource.key;
|
||||
|
||||
const routerName = `${key}-${resource.name}-router`;
|
||||
const serviceName = `${key}-${resource.name}-service`;
|
||||
@@ -665,7 +671,10 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -793,7 +802,10 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return targets
|
||||
|
||||
@@ -32,7 +32,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/access/export",
|
||||
description: "Export the access audit log for an organization as CSV",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryAccessAuditLogsParams
|
||||
|
||||
@@ -32,7 +32,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/action/export",
|
||||
description: "Export the action audit log for an organization as CSV",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryActionAuditLogsQuery,
|
||||
params: queryActionAuditLogsParams
|
||||
|
||||
@@ -249,7 +249,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/access",
|
||||
description: "Query the access audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryAccessAuditLogsParams
|
||||
|
||||
@@ -160,7 +160,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/action",
|
||||
description: "Query the action audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryActionAuditLogsQuery,
|
||||
params: queryActionAuditLogsParams
|
||||
|
||||
@@ -31,16 +31,16 @@ const getOrgSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/billing/usage",
|
||||
description: "Get an organization's billing usage",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: getOrgSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
// registry.registerPath({
|
||||
// method: "get",
|
||||
// path: "/org/{orgId}/billing/usage",
|
||||
// description: "Get an organization's billing usage",
|
||||
// tags: [OpenAPITags.Org],
|
||||
// request: {
|
||||
// params: getOrgSchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
|
||||
export async function getOrgUsage(
|
||||
req: Request,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
||||
import { getSubType } from "./getSubType";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
|
||||
@@ -172,7 +171,7 @@ export async function handleSubscriptionCreated(
|
||||
const email = orgUserRes.user.email;
|
||||
|
||||
if (email) {
|
||||
moveEmailToAudience(email, AudienceIds.Subscribed);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
}
|
||||
} else if (type === "license") {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
||||
import { getSubType } from "./getSubType";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import privateConfig from "#private/lib/config";
|
||||
@@ -109,7 +108,7 @@ export async function handleSubscriptionDeleted(
|
||||
const email = orgUserRes.user.email;
|
||||
|
||||
if (email) {
|
||||
moveEmailToAudience(email, AudienceIds.Churned);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
}
|
||||
} else if (type === "license") {
|
||||
|
||||
@@ -52,7 +52,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/idp/oidc",
|
||||
description: "Create an OIDC IdP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.OrgIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -35,7 +35,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/org/{orgId}/idp/{idpId}",
|
||||
description: "Delete IDP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.OrgIdp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -50,9 +50,9 @@ async function query(idpId: number, orgId: string) {
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/:orgId/idp/:idpId",
|
||||
path: "/org/{orgId}/idp/{idpId}",
|
||||
description: "Get an IDP by its IDP ID for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.OrgIdp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/idp",
|
||||
description: "List all IDP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.OrgIdp],
|
||||
request: {
|
||||
query: querySchema,
|
||||
params: paramsSchema
|
||||
|
||||
@@ -59,7 +59,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/idp/{idpId}/oidc",
|
||||
description: "Update an OIDC IdP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.OrgIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -52,7 +52,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/maintenance/info",
|
||||
description: "Get maintenance information for a resource by domain.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
query: z.object({
|
||||
fullDomain: z.string()
|
||||
|
||||
@@ -43,7 +43,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/access-token",
|
||||
description: "Generate a new access token for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.AccessToken],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: generateAccssTokenParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -122,7 +122,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/access-tokens",
|
||||
description: "List all access tokens in an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.AccessToken],
|
||||
tags: [OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
@@ -135,8 +135,8 @@ registry.registerPath({
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/access-tokens",
|
||||
description: "List all access tokens in an organization.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.AccessToken],
|
||||
description: "List all access tokens for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourceId: z.number()
|
||||
|
||||
@@ -37,7 +37,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/api-key",
|
||||
description: "Create a new API key scoped to the organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
tags: [OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -18,7 +18,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}",
|
||||
description: "Delete an API key.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
tags: [OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
||||
description: "List all actions set for an API key.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
tags: [OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
query: querySchema
|
||||
|
||||
@@ -52,7 +52,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/api-keys",
|
||||
description: "List all API keys for an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
tags: [OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
query: querySchema
|
||||
|
||||
@@ -25,7 +25,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
||||
description:
|
||||
"Set actions for an API key. This will replace any existing actions.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
||||
tags: [OpenAPITags.ApiKey],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -20,7 +20,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/request",
|
||||
description: "Query the request audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery.omit({
|
||||
limit: true,
|
||||
|
||||
@@ -151,7 +151,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/analytics",
|
||||
description: "Query the request audit analytics for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryRequestAuditLogsParams
|
||||
|
||||
@@ -182,7 +182,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/request",
|
||||
description: "Query the request audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Logs],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryRequestAuditLogsParams
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { db, users } from "@server/db";
|
||||
import { bannedEmails, bannedIps, db, users } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { email, z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
@@ -22,7 +22,6 @@ import { checkValidInvite } from "@server/auth/checkValidInvite";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { build } from "@server/build";
|
||||
import resend, { AudienceIds, moveEmailToAudience } from "#dynamic/lib/resend";
|
||||
|
||||
export const signupBodySchema = z.object({
|
||||
email: z.email().toLowerCase(),
|
||||
@@ -66,6 +65,30 @@ export async function signup(
|
||||
skipVerificationEmail
|
||||
} = parsedBody.data;
|
||||
|
||||
const [bannedEmail] = await db
|
||||
.select()
|
||||
.from(bannedEmails)
|
||||
.where(eq(bannedEmails.email, email))
|
||||
.limit(1);
|
||||
if (bannedEmail) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
||||
);
|
||||
}
|
||||
|
||||
if (req.ip) {
|
||||
const [bannedIp] = await db
|
||||
.select()
|
||||
.from(bannedIps)
|
||||
.where(eq(bannedIps.ip, req.ip))
|
||||
.limit(1);
|
||||
if (bannedIp) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const userId = generateId(15);
|
||||
|
||||
@@ -189,6 +212,7 @@ export async function signup(
|
||||
dateCreated: moment().toISOString(),
|
||||
termsAcceptedTimestamp: termsAcceptedTimestamp || null,
|
||||
termsVersion: "1",
|
||||
marketingEmailConsent: marketingEmailConsent ?? false,
|
||||
lastPasswordChange: new Date().getTime()
|
||||
});
|
||||
|
||||
@@ -212,7 +236,7 @@ export async function signup(
|
||||
logger.debug(
|
||||
`User ${email} opted in to marketing emails during signup.`
|
||||
);
|
||||
moveEmailToAudience(email, AudienceIds.SignUps);
|
||||
// TODO: update user in Sendy
|
||||
}
|
||||
|
||||
if (config.getRawConfig().flags?.require_email_verification) {
|
||||
|
||||
@@ -20,7 +20,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/blueprint",
|
||||
description: "Apply a base64 encoded JSON blueprint to an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
tags: [OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: applyBlueprintParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -43,7 +43,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/blueprint",
|
||||
description: "Create and apply a YAML blueprint to an organization",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
tags: [OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: applyBlueprintParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -53,7 +53,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/blueprint/{blueprintId}",
|
||||
description: "Get a blueprint by its blueprint ID.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
tags: [OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: getBlueprintSchema
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/blueprints",
|
||||
description: "List all blueprints for a organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
tags: [OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
|
||||
@@ -48,7 +48,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/client",
|
||||
description: "Create a new client for an organization.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: createClientParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -49,7 +49,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/user/{userId}/client",
|
||||
description:
|
||||
"Create a new client for a user and associate it with an existing olm.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Org, OpenAPITags.User],
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -243,7 +243,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/client/{niceId}",
|
||||
description:
|
||||
"Get a client by orgId and niceId. NiceId is a readable ID for the site and unique on a per org basis.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Site],
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
|
||||
@@ -119,12 +119,12 @@ const listClientsSchema = z.object({
|
||||
}),
|
||||
query: z.string().optional(),
|
||||
sort_by: z
|
||||
.enum(["megabytesIn", "megabytesOut"])
|
||||
.enum(["name", "megabytesIn", "megabytesOut"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["megabytesIn", "megabytesOut"],
|
||||
enum: ["name", "megabytesIn", "megabytesOut"],
|
||||
description: "Field to sort by"
|
||||
}),
|
||||
order: z
|
||||
@@ -237,7 +237,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/clients",
|
||||
description: "List all clients for an organization.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
query: listClientsSchema,
|
||||
params: listClientsParamsSchema
|
||||
@@ -363,14 +363,14 @@ export async function listClients(
|
||||
const countQuery = db.$count(baseQuery.as("filtered_clients"));
|
||||
|
||||
const listMachinesQuery = baseQuery
|
||||
.limit(page)
|
||||
.limit(pageSize)
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(
|
||||
sort_by
|
||||
? order === "asc"
|
||||
? asc(clients[sort_by])
|
||||
: desc(clients[sort_by])
|
||||
: asc(clients.clientId)
|
||||
: asc(clients.name)
|
||||
);
|
||||
|
||||
const [clientsList, totalCount] = await Promise.all([
|
||||
|
||||
@@ -256,7 +256,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/user-devices",
|
||||
description: "List all user devices for an organization.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
query: listUserDevicesSchema,
|
||||
params: listUserDevicesParamsSchema
|
||||
|
||||
@@ -23,7 +23,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/pick-client-defaults",
|
||||
description: "Return pre-requisite data for creating a client.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Site],
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: pickClientDefaultsSchema
|
||||
},
|
||||
|
||||
@@ -40,7 +40,8 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
|
||||
tries: domains.tries,
|
||||
configManaged: domains.configManaged,
|
||||
certResolver: domains.certResolver,
|
||||
preferWildcardCert: domains.preferWildcardCert
|
||||
preferWildcardCert: domains.preferWildcardCert,
|
||||
errorMessage: domains.errorMessage
|
||||
})
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId))
|
||||
@@ -59,7 +60,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/domains",
|
||||
description: "List all domains for a organization.",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Domain],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
||||
// Add site as a destination for this client
|
||||
const destination: PeerDestination = {
|
||||
destinationIP: site.subnet.split("/")[0],
|
||||
destinationPort: site.listenPort
|
||||
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
};
|
||||
|
||||
// Check if this destination is already in the array to avoid duplicates
|
||||
@@ -165,7 +165,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
||||
|
||||
const destination: PeerDestination = {
|
||||
destinationIP: peer.subnet.split("/")[0],
|
||||
destinationPort: peer.listenPort
|
||||
destinationPort: peer.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
};
|
||||
|
||||
// Check for duplicates
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function updateHolePunch(
|
||||
destinations: destinations
|
||||
});
|
||||
} catch (error) {
|
||||
// logger.error(error); // FIX THIS
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
@@ -262,7 +262,7 @@ export async function updateAndGenerateEndpointDestinations(
|
||||
if (site.subnet && site.listenPort) {
|
||||
destinations.push({
|
||||
destinationIP: site.subnet.split("/")[0],
|
||||
destinationPort: site.listenPort
|
||||
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -339,10 +339,10 @@ export async function updateAndGenerateEndpointDestinations(
|
||||
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
||||
}
|
||||
|
||||
if (!updatedSite || !updatedSite.subnet) {
|
||||
logger.warn(`Site not found: ${newt.siteId}`);
|
||||
throw new Error("Site not found");
|
||||
}
|
||||
// if (!updatedSite || !updatedSite.subnet) {
|
||||
// logger.warn(`Site not found: ${newt.siteId}`);
|
||||
// throw new Error("Site not found");
|
||||
// }
|
||||
|
||||
// Find all clients that connect to this site
|
||||
// const sitesClientPairs = await db
|
||||
|
||||
@@ -27,7 +27,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/idp/{idpId}/org/{orgId}",
|
||||
description: "Create an IDP policy for an existing IDP on an organization.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -37,7 +37,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/idp/oidc",
|
||||
description: "Create an OIDC IdP.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
|
||||
@@ -21,7 +21,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/idp/{idpId}",
|
||||
description: "Delete IDP.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/idp/{idpId}/org/{orgId}",
|
||||
description: "Create an OIDC IdP for an organization.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/idp/{idpId}",
|
||||
description: "Get an IDP by its IDP ID.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/idp/{idpId}/org",
|
||||
description: "List all org policies on an IDP.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
query: querySchema
|
||||
|
||||
@@ -58,7 +58,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/idp",
|
||||
description: "List all IDP in the system.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
query: querySchema
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/idp/{idpId}/org/{orgId}",
|
||||
description: "Update an IDP org policy.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -42,7 +42,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/idp/{idpId}/oidc",
|
||||
description: "Update an OIDC IdP.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
tags: [OpenAPITags.GlobalIdp],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits
|
||||
verifyLimits,
|
||||
verifyApiKeyDomainAccess
|
||||
} from "@server/middlewares";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Router } from "express";
|
||||
@@ -347,6 +348,56 @@ authenticated.get(
|
||||
domain.listDomains
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getDomain),
|
||||
domain.getDomain
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/domain",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrgDomain),
|
||||
logActionAudit(ActionsEnum.createOrgDomain),
|
||||
domain.createOrgDomain
|
||||
);
|
||||
|
||||
authenticated.patch(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
|
||||
domain.updateOrgDomain
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteOrgDomain),
|
||||
logActionAudit(ActionsEnum.deleteOrgDomain),
|
||||
domain.deleteAccountDomain
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId/dns-records",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getDNSRecords),
|
||||
domain.getDNSRecords
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/domain/:domainId/restart",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.restartOrgDomain),
|
||||
logActionAudit(ActionsEnum.restartOrgDomain),
|
||||
domain.restartOrgDomain
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/invitations",
|
||||
verifyApiKeyOrgAccess,
|
||||
|
||||
@@ -188,7 +188,8 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
||||
hcTimeout: targetHealthCheck.hcTimeout,
|
||||
hcHeaders: targetHealthCheck.hcHeaders,
|
||||
hcMethod: targetHealthCheck.hcMethod,
|
||||
hcTlsServerName: targetHealthCheck.hcTlsServerName
|
||||
hcTlsServerName: targetHealthCheck.hcTlsServerName,
|
||||
hcStatus: targetHealthCheck.hcStatus
|
||||
})
|
||||
.from(targets)
|
||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||
@@ -230,7 +231,7 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
||||
!target.hcMethod
|
||||
) {
|
||||
logger.debug(
|
||||
`Skipping target ${target.targetId} due to missing health check fields`
|
||||
`Skipping adding target health check ${target.targetId} due to missing health check fields`
|
||||
);
|
||||
return null; // Skip targets with missing health check fields
|
||||
}
|
||||
@@ -261,7 +262,8 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
||||
hcTimeout: target.hcTimeout, // in seconds
|
||||
hcHeaders: hcHeadersSend,
|
||||
hcMethod: target.hcMethod,
|
||||
hcTlsServerName: target.hcTlsServerName
|
||||
hcTlsServerName: target.hcTlsServerName,
|
||||
hcStatus: target.hcStatus
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -104,11 +104,11 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||
const payload = {
|
||||
oldDestination: {
|
||||
destinationIP: existingSite.subnet?.split("/")[0],
|
||||
destinationPort: existingSite.listenPort
|
||||
destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
},
|
||||
newDestination: {
|
||||
destinationIP: site.subnet?.split("/")[0],
|
||||
destinationPort: site.listenPort
|
||||
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist/add",
|
||||
description: "Add a single email to the resource whitelist.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: addEmailToResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -29,7 +29,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/roles/add",
|
||||
description: "Add a single role to a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: addRoleToResourceParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -29,7 +29,7 @@ registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/users/add",
|
||||
description: "Add a single user to a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: addUserToResourceParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -79,7 +79,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/resource",
|
||||
description: "Create a resource.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: createResourceParamsSchema,
|
||||
body: {
|
||||
@@ -223,6 +223,20 @@ async function createHttpResource(
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent creating resource with same domain as dashboard
|
||||
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
||||
if (dashboardUrl) {
|
||||
const dashboardHost = new URL(dashboardUrl).hostname;
|
||||
if (fullDomain === dashboardHost) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource domain cannot be the same as the dashboard domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (build != "oss") {
|
||||
const existingLoginPages = await db
|
||||
.select()
|
||||
|
||||
@@ -31,7 +31,7 @@ registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/rule",
|
||||
description: "Create a resource rule.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: createResourceRuleParamsSchema,
|
||||
body: {
|
||||
|
||||
@@ -22,7 +22,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Delete a resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: deleteResourceSchema
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}/rule/{ruleId}",
|
||||
description: "Delete a resource rule.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: deleteResourceRuleSchema
|
||||
},
|
||||
|
||||
@@ -54,7 +54,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/resource/{niceId}",
|
||||
description:
|
||||
"Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
@@ -68,7 +68,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}",
|
||||
description: "Get a resource by resourceId.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourceId: z.number()
|
||||
|
||||
@@ -31,7 +31,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/whitelist",
|
||||
description: "Get the whitelist of emails for a specific resource.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: getResourceWhitelistSchema
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resources-names",
|
||||
description: "List all resource names for an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
|
||||
@@ -35,7 +35,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/roles",
|
||||
description: "List all roles for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Role],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||
request: {
|
||||
params: listResourceRolesSchema
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/rules",
|
||||
description: "List rules for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.Rule],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
|
||||
request: {
|
||||
params: listResourceRulesParamsSchema,
|
||||
query: listResourceRulesSchema
|
||||
|
||||
@@ -38,7 +38,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/users",
|
||||
description: "List all users for a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.User],
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||
request: {
|
||||
params: listResourceUsersSchema
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user