Compare commits

..

No commits in common. "main" and "1.20.0" have entirely different histories.
main ... 1.20.0

83 changed files with 311 additions and 1675 deletions

View file

@ -41,7 +41,7 @@
</strong>
</p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control.
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
## Installation
@ -63,26 +63,11 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform.
* Lightweight user-space connector runs anywhere
* Punches through any firewall
* Doesn't require open ports or a public IP
* Strict network segmentation
* WireGuard-based
* Get alerts when a device or network resource goes down
<img src="public/screenshots/sites.png" alt="Sites" width="100%" />
### Browser-based reverse proxy access
Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the browser through identity and context-aware tunneled reverse proxies. Users access resources with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
* Expose a web panel anywhere
* Access via any web browser
* Single sign-on across all resources
* HTTPS resources
* Remote desktop in the browser with VNC and RDP
* In-browser SSH terminal with privileged access management (PAM)
* PIN codes, passcodes, email OTP, geoblocking, allow-lists, and more
Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
<img src="public/clip.gif" alt="Reverse proxy access" width="100%" />
@ -90,35 +75,14 @@ Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the b
Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
* Peer-to-peer with intelligent NAT traversal
* Hosts/IPs and port ranges
* Network ranges/CIDRs
* Friendly DNS aliases for network addresses
* Privileged access management (PAM) with SSH resources
* Private HTTPS resources only accessible on the private network
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
### Give users and roles access to resources
Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
* Bring your existing identity provider (IdP) or use Pangolin identities
* Sync users and roles from your IdP
* User- and role-based access control
* Full network audit and access logs
Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
<img src="public/screenshots/users.png" alt="Users from identity provider with roles" width="100%" />
### Find and launch resources from a personalized home page
Give users a landing page to quickly find and open the resources they can access. Resources are grouped by site or label, searchable, and filterable, with grid or list views. Saved views capture filters, grouping, and layout as personal or organization-wide defaults.
* Single place for admins and non-admins to see accessible resources
* Create reusable views for common access patterns
<img src="public/screenshots/resource-launcher.png" alt="Resource Launcher" width="100%" />
## Download Clients
Download the Pangolin client for your platform:

View file

@ -449,14 +449,8 @@
"provisioningManage": "Осигуряване",
"provisioningDescription": "Управление на ключовете за осигуряване и преглед на чаканещите сайтове за одобрение.",
"pendingSites": "Чаканещи сайтове",
"siteApproveSuccess": "Сайтът и свързаните ресурси са одобрени успешно",
"siteApproveSuccess": "Сайтът е одобрен успешно",
"siteApproveError": "Грешка при одобряването на сайта",
"siteReject": "Отказ на сайт",
"siteQuestionReject": "Сигурни ли сте, че искате да откажете този сайт?",
"siteMessageReject": "Това ще изтрие окончателно сайта и всички свързани ресурси, които все още са на изчакване.",
"siteConfirmReject": "Потвърдете отказ на сайт",
"siteRejectSuccess": "Сайтът беше успешно отхвърлен",
"siteRejectError": "Грешка при отхвърляне на сайта",
"provisioningKeys": "Ключове за осигуряване",
"searchProvisioningKeys": "Търсене на ключове за осигуряване...",
"provisioningKeysAdd": "Генериране на ключ за осигуряване",
@ -472,8 +466,8 @@
"provisioningKeysSave": "Запазете ключа за осигуряване",
"provisioningKeysSaveDescription": "Ще можете да видите това само веднъж. Копирайте го на сигурно място.",
"provisioningKeysErrorCreate": "Грешка при създаване на ключ за осигуряване",
"provisioningKeysList": "Нов ключ за разпределяне",
"provisioningKeysMaxBatchSize": "Максимален размер на пакета",
"provisioningKeysList": "Нов ключ за осигуряване",
"provisioningKeysMaxBatchSize": "Максимален размер на пакет",
"provisioningKeysUnlimitedBatchSize": "Неограничен размер на партида (без лимит)",
"provisioningKeysMaxBatchUnlimited": "Неограничено",
"provisioningKeysMaxBatchSizeInvalid": "Въведете валиден максимален размер на партида (11,000,000).",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Никога",
"provisioningKeysEdit": "Редактиране на ключ за осигуряване",
"provisioningKeysEditDescription": "Актуализирайте максималния размер на партида и времето на изтичане за този ключ.",
"provisioningKeysApproveNewSites": "Одобряване на нови сайтове",
"provisioningKeysApproveNewSites": "Одобрете нови сайтове",
"provisioningKeysApproveNewSitesDescription": "Автоматично одобряване на сайтове, които се регистрират с този ключ.",
"provisioningKeysUpdateError": "Грешка при актуализирането на ключа за осигуряване",
"provisioningKeysUpdated": "Ключът за осигуряване е актуализиран",
@ -1403,7 +1397,6 @@
"createOrgUser": "Създаване на Организационна Потребител",
"actionUpdateOrg": "Актуализиране на организацията",
"actionRemoveInvitation": "Премахване на поканата.",
"actionRemoveUserRole": "Премахване на роля на потребител",
"actionUpdateUser": "Актуализиране на потребител",
"actionGetUser": "Получаване на потребител",
"actionGetOrgUser": "Вземете потребител на организация",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
"actionUpdateSite": "Актуализиране на сайт",
"actionApproveSite": "Одобряване на сайт",
"actionRejectSite": "Отказ на сайт",
"actionResetSiteBandwidth": "Нулиране на честотната лента на организацията",
"actionListSiteRoles": "Изброяване на позволените роли за сайта",
"actionCreateResource": "Създаване на ресурс",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Въведете кода от приложението за удостоверяване или един от вашите резервни кодове за еднократна употреба.",
"otpAuthSubmit": "Изпрати код",
"idpContinue": "Или продължете със",
"idpLastUsed": "Последно използвано",
"otpAuthBack": "Назад към парола",
"navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Zajištění",
"provisioningDescription": "Spravovat klíče pro nastavení a zkontrolovat čekající stránky čekající na schválení.",
"pendingSites": "Nevyřízené weby",
"siteApproveSuccess": "Stránka a přidružené zdroje byly úspěšně schváleny",
"siteApproveSuccess": "Web byl úspěšně schválen",
"siteApproveError": "Chyba při schvalování webu",
"siteReject": "Odmítnout stránku",
"siteQuestionReject": "Opravdu chcete tuto stránku odmítnout?",
"siteMessageReject": "Toto trvale odstraní stránku a všechny přidružené zdroje, které jsou stále nevyřízené.",
"siteConfirmReject": "Potvrdit odmítnutí stránky",
"siteRejectSuccess": "Stránka byla úspěšně odmítnuta",
"siteRejectError": "Chyba při odmítání stránky",
"provisioningKeys": "Poskytovací klíče",
"searchProvisioningKeys": "Hledat klíče k zajišťování...",
"provisioningKeysAdd": "Generovat zajišťovací klíč",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Uložit konfigurační klíč",
"provisioningKeysSaveDescription": "Toto můžete vidět pouze jednou. Zkopírujte ho na bezpečné místo.",
"provisioningKeysErrorCreate": "Chyba při vytváření doplňovacího klíče",
"provisioningKeysList": "Nový provisioning klíč",
"provisioningKeysList": "Nový klíč pro poskytování informací",
"provisioningKeysMaxBatchSize": "Maximální velikost dávky",
"provisioningKeysUnlimitedBatchSize": "Neomezená velikost šarže (bez omezení)",
"provisioningKeysMaxBatchUnlimited": "Bez omezení",
"provisioningKeysMaxBatchSizeInvalid": "Zadejte platnou maximální velikost šarže (11,000,000).",
"provisioningKeysValidUntil": "Platná do",
"provisioningKeysValidUntil": "Platné do",
"provisioningKeysValidUntilHint": "Ponechte prázdné, pokud vyprší platnost.",
"provisioningKeysValidUntilInvalid": "Zadejte platné datum a čas.",
"provisioningKeysNumUsed": "Časy použití",
@ -1403,7 +1397,6 @@
"createOrgUser": "Vytvořit Org uživatele",
"actionUpdateOrg": "Aktualizovat organizaci",
"actionRemoveInvitation": "Odstranit pozvání",
"actionRemoveUserRole": "Odstranit uživatelskou roli",
"actionUpdateUser": "Aktualizovat uživatele",
"actionGetUser": "Získat uživatele",
"actionGetOrgUser": "Získat uživatele organizace",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Zadejte nastavovací token z konzole serveru.",
"setupTokenRequired": "Je vyžadován token nastavení",
"actionUpdateSite": "Aktualizovat stránku",
"actionApproveSite": "Schválit stránku",
"actionRejectSite": "Odmítnout stránku",
"actionResetSiteBandwidth": "Resetovat šířku pásma organizace",
"actionListSiteRoles": "Seznam povolených rolí webu",
"actionCreateResource": "Vytvořit zdroj",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Zadejte kód z vaší autentizační aplikace nebo jeden z vlastních záložních kódů.",
"otpAuthSubmit": "Odeslat kód",
"idpContinue": "Nebo pokračovat s",
"idpLastUsed": "Naposled použito",
"otpAuthBack": "Zpět na heslo",
"navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Provisionering",
"provisioningDescription": "Administrer provisioneringsnøgler og gennemgå ventende sites som venter på godkendelse.",
"pendingSites": "Ventende sites",
"siteApproveSuccess": "Site og tilknyttede ressourcer godkendt med succes",
"siteApproveSuccess": "Med succes godkendelse af site",
"siteApproveError": "Fejl ved godkendelse af side",
"siteReject": "Afvis Site",
"siteQuestionReject": "Er du sikker på, at du vil afvise dette site?",
"siteMessageReject": "Dette vil permanent slette sitet og enhver tilknyttet ressource, der stadig er i afventning.",
"siteConfirmReject": "Bekræft Afvis Site",
"siteRejectSuccess": "Site afvist med succes",
"siteRejectError": "Fejl ved afvisning af sitet",
"provisioningKeys": "Provisioneringsnøgler",
"searchProvisioningKeys": "Søg varer i provisioneringsnøgler...",
"provisioningKeysAdd": "Generer provisioneringsnøgle",
@ -473,7 +467,7 @@
"provisioningKeysSaveDescription": "Du kan kun se denne én gang. Kopiér det til et sikkert sted.",
"provisioningKeysErrorCreate": "Fejl under oprettelse af provisioneringsnøgle",
"provisioningKeysList": "Ny provisioneringsnøgle",
"provisioningKeysMaxBatchSize": "Maksimum Batch Størrelse",
"provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
"provisioningKeysUnlimitedBatchSize": "Ubegrænset mengde bunt (ingen begrænsning)",
"provisioningKeysMaxBatchUnlimited": "Ubegrænset",
"provisioningKeysMaxBatchSizeInvalid": "Angiv en gyldig sjakkstørrelse (11 000.000).",
@ -1403,7 +1397,6 @@
"createOrgUser": "Opret organisationsbruger",
"actionUpdateOrg": "Opdater organisation",
"actionRemoveInvitation": "Fjern invitation",
"actionRemoveUserRole": "Fjern Brugers Rolle",
"actionUpdateUser": "Opdater bruger",
"actionGetUser": "Hent bruger",
"actionGetOrgUser": "Hent organisationsbruger",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Indtast opsætningstoken fra serverkonsollen.",
"setupTokenRequired": "Opsætningstoken er nødvendig",
"actionUpdateSite": "Opdater site",
"actionApproveSite": "Godkend Site",
"actionRejectSite": "Afvis Site",
"actionResetSiteBandwidth": "Nulstil organisationsbåndbredde",
"actionListSiteRoles": "Vis tilladte områderoller",
"actionCreateResource": "Opret ressource",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Indtast koden fra autentiseringsappen din eller en af dine engangs reservekoder.",
"otpAuthSubmit": "Send kode",
"idpContinue": "Eller fortsæt med",
"idpLastUsed": "Sidst brugt",
"otpAuthBack": "Tilbage til adgangskode",
"navbar": "Navigationsmenu",
"navbarDescription": "Hovednavigasjonsmeny for applikationen",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Bereitstellung",
"provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Standorte prüfen, die noch auf Genehmigung warten.",
"pendingSites": "Ausstehende Standorte",
"siteApproveSuccess": "Site und zugehörige Ressourcen erfolgreich genehmigt",
"siteApproveSuccess": "Standort erfolgreich freigegeben",
"siteApproveError": "Fehler beim Genehmigen des Standorts",
"siteReject": "Site ablehnen",
"siteQuestionReject": "Sind Sie sicher, dass Sie diese Site ablehnen möchten?",
"siteMessageReject": "Dies wird die Site und alle damit verbundenen, noch ausstehenden Ressourcen dauerhaft löschen.",
"siteConfirmReject": "Ablehnung der Site bestätigen",
"siteRejectSuccess": "Site erfolgreich abgelehnt",
"siteRejectError": "Fehler beim Ablehnen der Site",
"provisioningKeys": "Bereitstellungsschlüssel",
"searchProvisioningKeys": "Bereitstellungsschlüssel suchen...",
"provisioningKeysAdd": "Bereitstellungsschlüssel generieren",
@ -473,7 +467,7 @@
"provisioningKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.",
"provisioningKeysErrorCreate": "Fehler beim Erstellen des Bereitstellungsschlüssels",
"provisioningKeysList": "Neuer Bereitstellungsschlüssel",
"provisioningKeysMaxBatchSize": "Maximale Batch-Größe",
"provisioningKeysMaxBatchSize": "Max. Batch-Größe",
"provisioningKeysUnlimitedBatchSize": "Unbegrenzte Batch-Größe (kein Limit)",
"provisioningKeysMaxBatchUnlimited": "Unbegrenzt",
"provisioningKeysMaxBatchSizeInvalid": "Geben Sie eine gültige maximale Batchgröße ein (11.000.000).",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Nie",
"provisioningKeysEdit": "Bereitstellungsschlüssel bearbeiten",
"provisioningKeysEditDescription": "Aktualisieren Sie die maximale Batch-Größe und Ablaufzeit für diesen Schlüssel.",
"provisioningKeysApproveNewSites": "Neue Sites genehmigen",
"provisioningKeysApproveNewSites": "Neuen Standort genehmigen",
"provisioningKeysApproveNewSitesDescription": "Sites, die sich mit diesem Schlüssel registrieren, automatisch freigeben.",
"provisioningKeysUpdateError": "Fehler beim Aktualisieren des Bereitstellungsschlüssels",
"provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert",
@ -1403,7 +1397,6 @@
"createOrgUser": "Org Benutzer erstellen",
"actionUpdateOrg": "Organisation aktualisieren",
"actionRemoveInvitation": "Einladung entfernen",
"actionRemoveUserRole": "Benutzerrolle entfernen",
"actionUpdateUser": "Benutzer aktualisieren",
"actionGetUser": "Benutzer abrufen",
"actionGetOrgUser": "Organisationsbenutzer abrufen",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
"setupTokenRequired": "Setup-Token ist erforderlich",
"actionUpdateSite": "Standorte aktualisieren",
"actionApproveSite": "Site genehmigen",
"actionRejectSite": "Site ablehnen",
"actionResetSiteBandwidth": "Organisations-Bandbreite zurücksetzen",
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
"actionCreateResource": "Ressource erstellen",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Geben Sie den Code aus Ihrer Authenticator-App oder einen Ihrer einmaligen Backup-Codes ein.",
"otpAuthSubmit": "Code absenden",
"idpContinue": "Oder weiter mit",
"idpLastUsed": "Zuletzt verwendet",
"otpAuthBack": "Zurück zum Passwort",
"navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site and associated resources approved successfully",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"siteReject": "Reject Site",
"siteQuestionReject": "Are you sure you want to reject this site?",
"siteMessageReject": "This will permanently delete the site and any associated resources that are still pending.",
"siteConfirmReject": "Confirm Reject Site",
"siteRejectSuccess": "Site rejected successfully",
"siteRejectError": "Error rejecting site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New Provisioning Key",
"provisioningKeysMaxBatchSize": "Max Batch Size",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid Until",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve New Sites",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
@ -1403,7 +1397,6 @@
"createOrgUser": "Create Org User",
"actionUpdateOrg": "Update Organization",
"actionRemoveInvitation": "Remove Invitation",
"actionRemoveUserRole": "Remove User Role",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Update Site",
"actionApproveSite": "Approve Site",
"actionRejectSite": "Reject Site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "List Allowed Site Roles",
"actionCreateResource": "Create Resource",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
"otpAuthSubmit": "Submit Code",
"idpContinue": "Or continue with",
"idpLastUsed": "Last Used",
"otpAuthBack": "Back to Password",
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Aprovisionamiento",
"provisioningDescription": "Administrar las claves de aprovisionamiento y revisar los sitios pendientes de aprobación.",
"pendingSites": "Sitios pendientes",
"siteApproveSuccess": "Sitio y recursos asociados aprobados correctamente",
"siteApproveSuccess": "Sitio aprobado con éxito",
"siteApproveError": "Error al aprobar el sitio",
"siteReject": "Rechazar Sitio",
"siteQuestionReject": "¿Está seguro de que desea rechazar este sitio?",
"siteMessageReject": "Esto eliminará permanentemente el sitio y cualquier recurso asociado que aún esté pendiente.",
"siteConfirmReject": "Confirmar Rechazo del Sitio",
"siteRejectSuccess": "Sitio rechazado correctamente",
"siteRejectError": "Error al rechazar el sitio",
"provisioningKeys": "Claves de aprovisionamiento",
"searchProvisioningKeys": "Buscar claves de suministro...",
"provisioningKeysAdd": "Generar clave de aprovisionamiento",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Guardar la clave de aprovisionamiento",
"provisioningKeysSaveDescription": "Sólo podrás verlo una vez. Copítalo a un lugar seguro.",
"provisioningKeysErrorCreate": "Error al crear la clave de provisioning",
"provisioningKeysList": "Nueva Clave de Provisión",
"provisioningKeysMaxBatchSize": "Tamaño Máximo del Lote",
"provisioningKeysList": "Nueva clave de aprovisionamiento",
"provisioningKeysMaxBatchSize": "Tamaño máximo de lote",
"provisioningKeysUnlimitedBatchSize": "Tamaño ilimitado del lote (sin límite)",
"provisioningKeysMaxBatchUnlimited": "Ilimitado",
"provisioningKeysMaxBatchSizeInvalid": "Introduzca un tamaño máximo de lote válido (11,000,000).",
"provisioningKeysValidUntil": "Válido Hasta",
"provisioningKeysValidUntil": "Válido hasta",
"provisioningKeysValidUntilHint": "Dejar vacío para no expirar.",
"provisioningKeysValidUntilInvalid": "Introduzca una fecha y hora válidas.",
"provisioningKeysNumUsed": "Tiempos usados",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Nunca",
"provisioningKeysEdit": "Editar clave de aprovisionamiento",
"provisioningKeysEditDescription": "Actualizar el tamaño máximo de lote y el tiempo de caducidad para esta clave.",
"provisioningKeysApproveNewSites": "Aprobar Nuevos Sitios",
"provisioningKeysApproveNewSites": "Aprobar nuevos sitios",
"provisioningKeysApproveNewSitesDescription": "Aprobar automáticamente los sitios que se registran con esta clave.",
"provisioningKeysUpdateError": "Error al actualizar la clave de aprovisionamiento",
"provisioningKeysUpdated": "Clave de aprovisionamiento actualizada",
@ -1403,7 +1397,6 @@
"createOrgUser": "Crear usuario Org",
"actionUpdateOrg": "Actualizar organización",
"actionRemoveInvitation": "Eliminar invitación",
"actionRemoveUserRole": "Quitar Rol de Usuario",
"actionUpdateUser": "Actualizar usuario",
"actionGetUser": "Obtener usuario",
"actionGetOrgUser": "Obtener usuario de la organización",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
"setupTokenRequired": "Se requiere el token de configuración",
"actionUpdateSite": "Actualizar sitio",
"actionApproveSite": "Aprobar Sitio",
"actionRejectSite": "Rechazar Sitio",
"actionResetSiteBandwidth": "Restablecer ancho de banda de la organización",
"actionListSiteRoles": "Lista de roles permitidos del sitio",
"actionCreateResource": "Crear Recurso",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Introduzca el código de su aplicación de autenticación o uno de sus códigos de copia de seguridad de un solo uso.",
"otpAuthSubmit": "Enviar código",
"idpContinue": "O continuar con",
"idpLastUsed": "Último Uso",
"otpAuthBack": "Volver a la contraseña",
"navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Mise en place",
"provisioningDescription": "Gérer les clés de provisioning et examiner les sites en attente d'approbation.",
"pendingSites": "Sites en attente",
"siteApproveSuccess": "Site et ressources associées approuvés avec succès",
"siteApproveSuccess": "Site approuvé avec succès",
"siteApproveError": "Erreur lors de l'approbation du site",
"siteReject": "Rejeter le site",
"siteQuestionReject": "Êtes-vous sûr de vouloir rejeter ce site ?",
"siteMessageReject": "Cela supprimera définitivement le site et toutes les ressources associées qui sont encore en attente.",
"siteConfirmReject": "Confirmer le rejet du site",
"siteRejectSuccess": "Site rejeté avec succès",
"siteRejectError": "Erreur lors du rejet du site",
"provisioningKeys": "Clés de provisionnement",
"searchProvisioningKeys": "Recherche des clés de provision...",
"provisioningKeysAdd": "Générer une clé de provisioning",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Enregistrer la clé de provisioning",
"provisioningKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Copiez-le dans un endroit sécurisé.",
"provisioningKeysErrorCreate": "Erreur lors de la création de la clé de provisioning",
"provisioningKeysList": "Nouvelle clé d'approvisionnement",
"provisioningKeysList": "Nouvelle clé de provisioning",
"provisioningKeysMaxBatchSize": "Taille maximale du lot",
"provisioningKeysUnlimitedBatchSize": "Taille de lot illimitée (sans limite)",
"provisioningKeysMaxBatchUnlimited": "Illimité",
"provisioningKeysMaxBatchSizeInvalid": "Entrez une taille de lot maximale valide (11 000 000).",
"provisioningKeysValidUntil": "Valable jusqu'à",
"provisioningKeysValidUntil": "Valable jusqu'au",
"provisioningKeysValidUntilHint": "Laisser vide pour ne pas expirer.",
"provisioningKeysValidUntilInvalid": "Entrez une date et une heure valides.",
"provisioningKeysNumUsed": "Nombre de fois utilisées",
@ -1403,7 +1397,6 @@
"createOrgUser": "Créer un utilisateur Org",
"actionUpdateOrg": "Mettre à jour l'organisation",
"actionRemoveInvitation": "Supprimer l'invitation",
"actionRemoveUserRole": "Supprimer le rôle d'utilisateur",
"actionUpdateUser": "Mettre à jour l'utilisateur",
"actionGetUser": "Obtenir l'utilisateur",
"actionGetOrgUser": "Obtenir l'utilisateur de l'organisation",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
"actionUpdateSite": "Mettre à jour un site",
"actionApproveSite": "Approuver le site",
"actionRejectSite": "Rejeter le site",
"actionResetSiteBandwidth": "Réinitialiser la bande passante de l'organisation",
"actionListSiteRoles": "Lister les rôles autorisés du site",
"actionCreateResource": "Créer une ressource",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Entrez le code de votre application d'authentification ou l'un de vos codes de secours à usage unique.",
"otpAuthSubmit": "Soumettre le code",
"idpContinue": "Ou continuer avec",
"idpLastUsed": "Dernière utilisation",
"otpAuthBack": "Retour au mot de passe",
"navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Accantonamento",
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
"pendingSites": "Siti In Attesa",
"siteApproveSuccess": "Sito e risorse associate approvate con successo",
"siteApproveSuccess": "Sito approvato con successo",
"siteApproveError": "Errore nell'approvazione del sito",
"siteReject": "Rifiuta Sito",
"siteQuestionReject": "Sei sicuro di voler rifiutare questo sito?",
"siteMessageReject": "Questo eliminerà permanentemente il sito e tutte le risorse associate ancora in sospeso.",
"siteConfirmReject": "Conferma Rifiuto Sito",
"siteRejectSuccess": "Sito rifiutato con successo",
"siteRejectError": "Errore nel rifiutare il sito",
"provisioningKeys": "Chiavi Di Provvedimento",
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
"provisioningKeysAdd": "Genera Chiave di provisioning",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Salva la chiave di provisioning",
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
"provisioningKeysList": "Nuova Chiave di Provisioning",
"provisioningKeysMaxBatchSize": "Dimensione Massima Batch",
"provisioningKeysList": "Nuova chiave di provisioning",
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
"provisioningKeysMaxBatchUnlimited": "Illimitato",
"provisioningKeysMaxBatchSizeInvalid": "Inserisci una dimensione massima valida del batch (11.000.000).",
"provisioningKeysValidUntil": "Valido Fino a",
"provisioningKeysValidUntil": "Valido fino al",
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
"provisioningKeysNumUsed": "Volte usate",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Mai",
"provisioningKeysEdit": "Modifica Chiave di provisioning",
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
"provisioningKeysApproveNewSites": "Approva Nuovi Siti",
"provisioningKeysApproveNewSites": "Approva nuovi siti",
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
"provisioningKeysUpdated": "Chiave di provisioning aggiornata",
@ -1403,7 +1397,6 @@
"createOrgUser": "Crea Utente Org",
"actionUpdateOrg": "Aggiorna Organizzazione",
"actionRemoveInvitation": "Rimuovi Invito",
"actionRemoveUserRole": "Rimuovi Ruolo Utente",
"actionUpdateUser": "Aggiorna Utente",
"actionGetUser": "Ottieni Utente",
"actionGetOrgUser": "Ottieni Utente Organizzazione",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
"setupTokenRequired": "Il token di configurazione è richiesto",
"actionUpdateSite": "Aggiorna Sito",
"actionApproveSite": "Approva Sito",
"actionRejectSite": "Rifiuta Sito",
"actionResetSiteBandwidth": "Reimposta Larghezza Banda Dell'Organizzazione",
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
"actionCreateResource": "Crea Risorsa",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Inserisci il codice dalla tua app di autenticazione o uno dei tuoi codici di backup monouso.",
"otpAuthSubmit": "Invia Codice",
"idpContinue": "O continua con",
"idpLastUsed": "Ultimo Utilizzo",
"otpAuthBack": "Torna alla Password",
"navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione",

View file

@ -449,14 +449,8 @@
"provisioningManage": "프로비저닝",
"provisioningDescription": "프로비저닝 키를 관리하고 승인을 기다리는 사이트를 검토합니다.",
"pendingSites": "대기중인 사이트",
"siteApproveSuccess": "사이트 및 관련 리소스가 성공적으로 승인되었습니다",
"siteApproveSuccess": "사이트가 성공적으로 승인되었습니다",
"siteApproveError": "사이트 승인 오류",
"siteReject": "사이트 거부",
"siteQuestionReject": "이 사이트를 거부하시겠습니까?",
"siteMessageReject": "이렇게 하면 펜딩 중인 사이트 및 관련 리소스가 영구적으로 삭제됩니다.",
"siteConfirmReject": "사이트 거부 확인",
"siteRejectSuccess": "사이트가 성공적으로 거부되었습니다",
"siteRejectError": "사이트 거부 오류",
"provisioningKeys": "프로비저닝 키",
"searchProvisioningKeys": "프로비저닝 키 검색...",
"provisioningKeysAdd": "프로비저닝 키 생성",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "절대",
"provisioningKeysEdit": "프로비저닝 키 수정",
"provisioningKeysEditDescription": "이 키의 최대 배치 크기 및 만료 시간을 업데이트하세요.",
"provisioningKeysApproveNewSites": "새 사이트 승인",
"provisioningKeysApproveNewSites": "새로운 사이트 승인",
"provisioningKeysApproveNewSitesDescription": "이 키를 등록하는 사이트를 자동으로 승인합니다.",
"provisioningKeysUpdateError": "프로비저닝 키 업데이트 오류",
"provisioningKeysUpdated": "프로비저닝 키가 업데이트되었습니다",
@ -1403,7 +1397,6 @@
"createOrgUser": "조직 사용자 생성",
"actionUpdateOrg": "조직 업데이트",
"actionRemoveInvitation": "초대 제거",
"actionRemoveUserRole": "사용자 역할 제거",
"actionUpdateUser": "사용자 업데이트",
"actionGetUser": "사용자 조회",
"actionGetOrgUser": "조직 사용자 가져오기",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
"actionUpdateSite": "사이트 업데이트",
"actionApproveSite": "사이트 승인",
"actionRejectSite": "사이트 거부",
"actionResetSiteBandwidth": "조직 대역폭 재설정",
"actionListSiteRoles": "허용된 사이트 역할 목록",
"actionCreateResource": "리소스 생성",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.",
"otpAuthSubmit": "코드 제출",
"idpContinue": "또는 계속 진행하십시오.",
"idpLastUsed": "마지막 사용",
"otpAuthBack": "비밀번호로 돌아가기",
"navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Levering",
"provisioningDescription": "Administrer foreløpig nøkler og gjennomgå ventende nettsteder som venter på godkjenning.",
"pendingSites": "Ventende nettsteder",
"siteApproveSuccess": "Område og tilknyttede ressurser godkjent",
"siteApproveSuccess": "Vellykket godkjenning av nettsted",
"siteApproveError": "Feil ved godkjenning av side",
"siteReject": "Avvis Område",
"siteQuestionReject": "Er du sikker på at du vil avvise dette området?",
"siteMessageReject": "Dette vil permanent slette området og eventuelle tilknyttede ressurser som fortsatt venter.",
"siteConfirmReject": "Bekreft Avvisning av Område",
"siteRejectSuccess": "Område avvist",
"siteRejectError": "Feil ved avvisning av området",
"provisioningKeys": "Foreløpig nøkler",
"searchProvisioningKeys": "Søk varer i lagrings nøkler...",
"provisioningKeysAdd": "Generer fremvisende nøkkel",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Lagre den midlertidig nøkkelen",
"provisioningKeysSaveDescription": "Du kan bare se denne én gang. Kopier det til et sikkert sted.",
"provisioningKeysErrorCreate": "Feil under oppretting av foreløpig nøkkel",
"provisioningKeysList": "Ny Forsyningsnøkkel",
"provisioningKeysMaxBatchSize": "Maks Batch Størrelse",
"provisioningKeysList": "Ny provisorisk nøkkel",
"provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
"provisioningKeysUnlimitedBatchSize": "Ubegrenset mengde bunt (ingen begrensning)",
"provisioningKeysMaxBatchUnlimited": "Ubegrenset",
"provisioningKeysMaxBatchSizeInvalid": "Angi en gyldig sjakkstørrelse (11 000.000).",
"provisioningKeysValidUntil": "Gyldig Til",
"provisioningKeysValidUntil": "Gyldig til",
"provisioningKeysValidUntilHint": "La stå tomt for ingen utløp.",
"provisioningKeysValidUntilInvalid": "Angi en gyldig dato og klokkeslett.",
"provisioningKeysNumUsed": "Antall ganger brukt",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Aldri",
"provisioningKeysEdit": "Rediger bestemmelsesnøkkel",
"provisioningKeysEditDescription": "Oppdater maksimal størrelse for bunt og utløpstid for denne nøkkelen.",
"provisioningKeysApproveNewSites": "Godkjenn Nye Områder",
"provisioningKeysApproveNewSites": "Godkjenn nye nettsteder",
"provisioningKeysApproveNewSitesDescription": "Godkjenn automatisk nettsteder som registrerer deg med denne nøkkelen.",
"provisioningKeysUpdateError": "Feil under oppdatering av foreløpig nøkkel",
"provisioningKeysUpdated": "Foreslå nøkkel oppdatert",
@ -1403,7 +1397,6 @@
"createOrgUser": "Opprett Org bruker",
"actionUpdateOrg": "Oppdater organisasjon",
"actionRemoveInvitation": "Fjern invitasjon",
"actionRemoveUserRole": "Fjern Brukerrolle",
"actionUpdateUser": "Oppdater bruker",
"actionGetUser": "Hent bruker",
"actionGetOrgUser": "Hent organisasjonsbruker",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig",
"actionUpdateSite": "Oppdater område",
"actionApproveSite": "Godkjenn Område",
"actionRejectSite": "Avvis Område",
"actionResetSiteBandwidth": "Tilbakestill organisasjons-båndbredde",
"actionListSiteRoles": "List opp tillatte områderoller",
"actionCreateResource": "Opprett ressurs",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.",
"otpAuthSubmit": "Send inn kode",
"idpContinue": "Eller fortsett med",
"idpLastUsed": "Sist brukt",
"otpAuthBack": "Tilbake til passord",
"navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Provisie",
"provisioningDescription": "Voorzieningssleutels beheren en sites beoordelen in afwachting van goedkeuring.",
"pendingSites": "Openstaande sites",
"siteApproveSuccess": "Site en bijbehorende middelen succesvol goedgekeurd",
"siteApproveSuccess": "Site succesvol goedgekeurd",
"siteApproveError": "Fout bij goedkeuren website",
"siteReject": "Afwijzen Site",
"siteQuestionReject": "Weet u zeker dat u deze site wilt afwijzen?",
"siteMessageReject": "Hiermee wordt de site en alle bijbehorende middelen die nog in behandeling zijn permanent verwijderd.",
"siteConfirmReject": "Afwijzing van site bevestigen",
"siteRejectSuccess": "Site succesvol afgewezen",
"siteRejectError": "Fout bij het afwijzen van site",
"provisioningKeys": "Verhelderende sleutels",
"searchProvisioningKeys": "Zoek provisioningsleutels ...",
"provisioningKeysAdd": "Genereer Provisioning Sleutel",
@ -472,7 +466,7 @@
"provisioningKeysSave": "Sla de provisioning sleutel op",
"provisioningKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plaats.",
"provisioningKeysErrorCreate": "Fout bij aanmaken provisioning sleutel",
"provisioningKeysList": "Nieuwe provisioning-sleutel",
"provisioningKeysList": "Nieuwe provisioning sleutel",
"provisioningKeysMaxBatchSize": "Maximale batchgrootte",
"provisioningKeysUnlimitedBatchSize": "Onbeperkte batchgrootte (geen limiet)",
"provisioningKeysMaxBatchUnlimited": "Onbeperkt",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Nooit",
"provisioningKeysEdit": "Wijzig Provisioning Sleutel",
"provisioningKeysEditDescription": "Werk de maximale batchgrootte en verlooptijd voor deze sleutel bij.",
"provisioningKeysApproveNewSites": "Nieuwe sites goedkeuren",
"provisioningKeysApproveNewSites": "Goedkeuren van nieuwe sites",
"provisioningKeysApproveNewSitesDescription": "Automatisch sites goedkeuren die zich registreren met deze sleutel.",
"provisioningKeysUpdateError": "Fout tijdens bijwerken provisioning sleutel",
"provisioningKeysUpdated": "Provisie sleutel bijgewerkt",
@ -1403,7 +1397,6 @@
"createOrgUser": "Org gebruiker aanmaken",
"actionUpdateOrg": "Organisatie bijwerken",
"actionRemoveInvitation": "Verwijder uitnodiging",
"actionRemoveUserRole": "Gebruikersrol verwijderen",
"actionUpdateUser": "Gebruiker bijwerken",
"actionGetUser": "Gebruiker ophalen",
"actionGetOrgUser": "Krijg organisatie-gebruiker",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist",
"actionUpdateSite": "Site bijwerken",
"actionApproveSite": "Verleen site goedkeuring",
"actionRejectSite": "Afwijzen Site",
"actionResetSiteBandwidth": "Reset organisatieschandbreedte",
"actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Voer de code van je authenticator-app of een van je reservekopiecodes voor het eenmalig gebruik in.",
"otpAuthSubmit": "Code indienen",
"idpContinue": "Of ga verder met",
"idpLastUsed": "Laatst gebruikt",
"otpAuthBack": "Terug naar wachtwoord",
"navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Dostarczanie",
"provisioningDescription": "Zarządzaj kluczami rezerwacji i sprawdzaj oczekujące strony oczekujące na zatwierdzenie.",
"pendingSites": "Witryny oczekujące",
"siteApproveSuccess": "Witryna i powiązane zasoby zostały zatwierdzone pomyślnie",
"siteApproveSuccess": "Witryna została pomyślnie zatwierdzona",
"siteApproveError": "Błąd zatwierdzania witryny",
"siteReject": "Odrzuć witrynę",
"siteQuestionReject": "Czy na pewno chcesz odrzucić tę witrynę?",
"siteMessageReject": "Spowoduje to trwałe usunięcie witryny i wszelkich powiązanych, nadal oczekujących zasobów.",
"siteConfirmReject": "Potwierdź odrzucenie witryny",
"siteRejectSuccess": "Witryna została odrzucona pomyślnie",
"siteRejectError": "Błąd podczas odrzucania witryny",
"provisioningKeys": "Klucze Zaopatrzenia",
"searchProvisioningKeys": "Szukaj kluczy zaopatrzenia...",
"provisioningKeysAdd": "Wygeneruj klucz zaopatrzenia",
@ -472,7 +466,7 @@
"provisioningKeysSave": "Zapisz klucz zaopatrzenia",
"provisioningKeysSaveDescription": "Możesz to zobaczyć tylko raz. Skopiuj je do bezpiecznego miejsca.",
"provisioningKeysErrorCreate": "Błąd podczas tworzenia klucza zaopatrzenia",
"provisioningKeysList": "Nowy klucz aprowizacyjny",
"provisioningKeysList": "Nowy klucz rezerwacji",
"provisioningKeysMaxBatchSize": "Maksymalny rozmiar partii",
"provisioningKeysUnlimitedBatchSize": "Nieograniczony rozmiar partii (bez limitu)",
"provisioningKeysMaxBatchUnlimited": "Nieograniczona",
@ -1403,7 +1397,6 @@
"createOrgUser": "Utwórz użytkownika Org",
"actionUpdateOrg": "Aktualizuj organizację",
"actionRemoveInvitation": "Usuń zaproszenie",
"actionRemoveUserRole": "Usuń rolę użytkownika",
"actionUpdateUser": "Zaktualizuj użytkownika",
"actionGetUser": "Pobierz użytkownika",
"actionGetOrgUser": "Pobierz użytkownika organizacji",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
"setupTokenRequired": "Wymagany jest token konfiguracji",
"actionUpdateSite": "Aktualizuj witrynę",
"actionApproveSite": "Zatwierdź witrynę",
"actionRejectSite": "Odrzuć witrynę",
"actionResetSiteBandwidth": "Zresetuj przepustowość organizacji",
"actionListSiteRoles": "Lista dozwolonych ról witryny",
"actionCreateResource": "Utwórz zasób",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Wprowadź kod z aplikacji uwierzytelniającej lub jeden z jednorazowych kodów zapasowych.",
"otpAuthSubmit": "Wyślij kod",
"idpContinue": "Lub kontynuuj z",
"idpLastUsed": "Ostatnio używany",
"otpAuthBack": "Powrót do hasła",
"navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Provisionamento",
"provisioningDescription": "Gerenciar chaves de provisionamento e revisar sites pendentes aguardando aprovação.",
"pendingSites": "Sites pendentes",
"siteApproveSuccess": "Site e recursos associados aprovados com sucesso",
"siteApproveSuccess": "Site aprovado com sucesso",
"siteApproveError": "Erro ao aprovar site",
"siteReject": "Rejeitar Site",
"siteQuestionReject": "Tem certeza de que deseja rejeitar este site?",
"siteMessageReject": "Isto eliminará permanentemente o site e quaisquer recursos associados que ainda estejam pendentes.",
"siteConfirmReject": "Confirmar Rejeição de Site",
"siteRejectSuccess": "Site rejeitado com sucesso",
"siteRejectError": "Erro ao rejeitar site",
"provisioningKeys": "Posicionando chaves",
"searchProvisioningKeys": "Pesquisar chaves de provisionamento...",
"provisioningKeysAdd": "Gerar chave de provisionamento",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Salvar a chave de provisionamento",
"provisioningKeysSaveDescription": "Você só será capaz de ver esta vez. Copiá-lo para um lugar seguro.",
"provisioningKeysErrorCreate": "Erro ao criar chave de provisionamento",
"provisioningKeysList": "Nova Chave de Provisionamento",
"provisioningKeysMaxBatchSize": "Tamanho Máximo do Lote",
"provisioningKeysList": "Nova chave de aprovisionamento",
"provisioningKeysMaxBatchSize": "Tamanho máximo do lote",
"provisioningKeysUnlimitedBatchSize": "Tamanho ilimitado em lote (sem limite)",
"provisioningKeysMaxBatchUnlimited": "Ilimitado",
"provisioningKeysMaxBatchSizeInvalid": "Informe um tamanho máximo válido em lote (11,000,000).",
"provisioningKeysValidUntil": "Válido Até",
"provisioningKeysValidUntil": "Valido ate",
"provisioningKeysValidUntilHint": "Deixe em branco para nenhuma expiração.",
"provisioningKeysValidUntilInvalid": "Informe uma data e hora válidas.",
"provisioningKeysNumUsed": "Use percentual",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "nunca",
"provisioningKeysEdit": "Editar chave de provisionamento",
"provisioningKeysEditDescription": "Atualizar o tamanho máximo do lote e tempo de expiração para esta chave.",
"provisioningKeysApproveNewSites": "Aprovar Novos Sites",
"provisioningKeysApproveNewSites": "Aprovar novos sites",
"provisioningKeysApproveNewSitesDescription": "Aprovar automaticamente sites que se registram com esta chave.",
"provisioningKeysUpdateError": "Erro ao atualizar chave de provisionamento",
"provisioningKeysUpdated": "Chave de provisionamento atualizada",
@ -1403,7 +1397,6 @@
"createOrgUser": "Criar utilizador Org",
"actionUpdateOrg": "Atualizar Organização",
"actionRemoveInvitation": "Remover Convite",
"actionRemoveUserRole": "Remover Função de Utilizador",
"actionUpdateUser": "Atualizar Usuário",
"actionGetUser": "Obter Usuário",
"actionGetOrgUser": "Obter Utilizador da Organização",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
"setupTokenRequired": "Token de configuração é necessário",
"actionUpdateSite": "Atualizar Site",
"actionApproveSite": "Aprovar Site",
"actionRejectSite": "Rejeitar Site",
"actionResetSiteBandwidth": "Redefinir banda da organização",
"actionListSiteRoles": "Listar Funções Permitidas do Site",
"actionCreateResource": "Criar Recurso",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Insira o código da sua aplicação de autenticação ou um dos seus códigos de backup de uso único.",
"otpAuthSubmit": "Submeter Código",
"idpContinue": "Ou continuar com",
"idpLastUsed": "Última Utilização",
"otpAuthBack": "Voltar à Palavra-passe",
"navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Подготовка",
"provisioningDescription": "Управляйте предоставленными ключами и проверять непроверенные сайты, ожидающие утверждения.",
"pendingSites": "Ожидающие сайты",
"siteApproveSuccess": "Сайт и связанные ресурсы успешно одобрены",
"siteApproveSuccess": "Сайт успешно утвержден",
"siteApproveError": "Ошибка при утверждении сайта",
"siteReject": "Отклонить сайт",
"siteQuestionReject": "Вы уверены, что хотите отклонить этот сайт?",
"siteMessageReject": "Этот процесс окончательно удалит сайт и все связанные с ним ресурсы, которые еще ожидают.",
"siteConfirmReject": "Подтвердите отклонение сайта",
"siteRejectSuccess": "Сайт успешно отклонен",
"siteRejectError": "Ошибка при отклонении сайта",
"provisioningKeys": "Ключи подготовки",
"searchProvisioningKeys": "Поиск подготовительных ключей...",
"provisioningKeysAdd": "Сгенерировать ключ подготовки",
@ -472,8 +466,8 @@
"provisioningKeysSave": "Сохранить ключ подготовки",
"provisioningKeysSaveDescription": "Вы сможете увидеть это только один раз. Скопируйте его в безопасное место.",
"provisioningKeysErrorCreate": "Ошибка при создании ключа подготовки",
"provisioningKeysList": "Новый ключ для подготовки",
"provisioningKeysMaxBatchSize": "Максимальный размер партии",
"provisioningKeysList": "Новый подготовительный ключ",
"provisioningKeysMaxBatchSize": "Макс. размер партии",
"provisioningKeysUnlimitedBatchSize": "Неограниченный размер партии (без ограничений)",
"provisioningKeysMaxBatchUnlimited": "Неограниченный",
"provisioningKeysMaxBatchSizeInvalid": "Введите максимальный размер пакета (11,000,000).",
@ -1403,7 +1397,6 @@
"createOrgUser": "Создать пользователя Org",
"actionUpdateOrg": "Обновить организацию",
"actionRemoveInvitation": "Удалить приглашение",
"actionRemoveUserRole": "Удалить роль пользователя",
"actionUpdateUser": "Обновить пользователя",
"actionGetUser": "Получить пользователя",
"actionGetOrgUser": "Получить пользователя организации",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
"setupTokenRequired": "Токен настройки обязателен",
"actionUpdateSite": "Обновить сайт",
"actionApproveSite": "Одобрить сайт",
"actionRejectSite": "Отклонить сайт",
"actionResetSiteBandwidth": "Сброс пропускной способности организации",
"actionListSiteRoles": "Список разрешенных ролей сайта",
"actionCreateResource": "Создать ресурс",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Введите код из вашего приложения-аутентификатора или один из ваших одноразовых резервных кодов.",
"otpAuthSubmit": "Отправить код",
"idpContinue": "Или продолжить с",
"idpLastUsed": "Последнее использование",
"otpAuthBack": "Назад к паролю",
"navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения",

View file

@ -449,14 +449,8 @@
"provisioningManage": "Tedarik",
"provisioningDescription": "Tedarik anahtarlarını yönetin ve onay bekleyen siteleri gözden geçirin.",
"pendingSites": "Bekleyen Siteler",
"siteApproveSuccess": "Site ve ilgili kaynaklar başarıyla onaylandı",
"siteApproveSuccess": "Site başarıyla onaylandı",
"siteApproveError": "Site onaylanırken hata oluştu",
"siteReject": "Siteyi Reddet",
"siteQuestionReject": "Bu siteyi reddetmek istediğinizden emin misiniz?",
"siteMessageReject": "Bu işlem, siteyi ve hala beklemede olan herhangi bir ilgili kaynağı kalıcı olarak silecektir.",
"siteConfirmReject": "Site Reddetmeyi Onayla",
"siteRejectSuccess": "Site başarıyla reddedildi",
"siteRejectError": "Site reddedilirken hata oluştu",
"provisioningKeys": "Tedarik Anahtarları",
"searchProvisioningKeys": "Tedarik anahtarlarını ara...",
"provisioningKeysAdd": "Tedarik Anahtarı Üret",
@ -472,12 +466,12 @@
"provisioningKeysSave": "Tedarik anahtarını kaydet",
"provisioningKeysSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyalayın.",
"provisioningKeysErrorCreate": "Tedarik anahtarı oluşturulurken hata oluştu",
"provisioningKeysList": "Yeni Sağlama Anahtarı",
"provisioningKeysMaxBatchSize": "Maksimum Toplu İş Boyutu",
"provisioningKeysList": "Yeni tedarik anahtarı",
"provisioningKeysMaxBatchSize": "Maksimum toplu iş boyutu",
"provisioningKeysUnlimitedBatchSize": "Sınırsız toplu iş boyutu (sınırlama yok)",
"provisioningKeysMaxBatchUnlimited": "Sınırsız",
"provisioningKeysMaxBatchSizeInvalid": "Geçerli bir maksimum toplu iş boyutu girin (11,000,000).",
"provisioningKeysValidUntil": "Geçerli Olma Süresi",
"provisioningKeysValidUntil": "Geçerlilik tarihi",
"provisioningKeysValidUntilHint": "Son kullanım tarihi için boş bırakın.",
"provisioningKeysValidUntilInvalid": "Geçerli bir tarih ve saat girin.",
"provisioningKeysNumUsed": "Kullanım Sayısı",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "Asla",
"provisioningKeysEdit": "Tedarik Anahtarını Düzenle",
"provisioningKeysEditDescription": "Bu anahtar için maksimum toplu iş boyutunu ve son kullanma zamanını güncelleyin.",
"provisioningKeysApproveNewSites": "Yeni Siteleri Onayla",
"provisioningKeysApproveNewSites": "Yeni siteleri onayla",
"provisioningKeysApproveNewSitesDescription": "Bu anahtar ile kayıt olan siteleri otomatik olarak onayla.",
"provisioningKeysUpdateError": "Tedarik anahtarı güncellenirken hata oluştu",
"provisioningKeysUpdated": "Tedarik anahtarı güncellendi",
@ -1403,7 +1397,6 @@
"createOrgUser": "Organizasyon Kullanıcısı Oluştur",
"actionUpdateOrg": "Kuruluşu Güncelle",
"actionRemoveInvitation": "Daveti Kaldır",
"actionRemoveUserRole": "Kullanıcı Rolünü Kaldır",
"actionUpdateUser": "Kullanıcıyı Güncelle",
"actionGetUser": "Kullanıcıyı Getir",
"actionGetOrgUser": "Kuruluş Kullanıcısını Al",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
"actionUpdateSite": "Siteyi Güncelle",
"actionApproveSite": "Siteyi Onayla",
"actionRejectSite": "Siteyi Reddet",
"actionResetSiteBandwidth": "Organizasyon Bant Genişliğini Sıfırla",
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
"actionCreateResource": "Kaynak Oluştur",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.",
"otpAuthSubmit": "Kodu Gönder",
"idpContinue": "Veya devam et:",
"idpLastUsed": "Son Kullanılan",
"otpAuthBack": "Şifreye Geri Dön",
"navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü",

View file

@ -449,14 +449,8 @@
"provisioningManage": "预配",
"provisioningDescription": "管理预配密钥,并审核待批准的站点。",
"pendingSites": "待审批站点",
"siteApproveSuccess": "站点及相关资源已成功审批",
"siteApproveSuccess": "站点准成功",
"siteApproveError": "批准站点出错",
"siteReject": "拒绝站点",
"siteQuestionReject": "您确定要拒绝此站点吗?",
"siteMessageReject": "这将永久删除站点及所有尚未处理的相关资源。",
"siteConfirmReject": "确认拒绝站点",
"siteRejectSuccess": "站点已被成功拒绝",
"siteRejectError": "拒绝站点时出错",
"provisioningKeys": "置备键",
"searchProvisioningKeys": "搜索配备密钥...",
"provisioningKeysAdd": "生成预配密钥",
@ -472,8 +466,8 @@
"provisioningKeysSave": "保存预配键",
"provisioningKeysSaveDescription": "您只能看到一次。复制它到一个安全的地方。",
"provisioningKeysErrorCreate": "创建预配键时出错",
"provisioningKeysList": "新预配密钥",
"provisioningKeysMaxBatchSize": "最大批大小",
"provisioningKeysList": "新建预配键",
"provisioningKeysMaxBatchSize": "最大批大小",
"provisioningKeysUnlimitedBatchSize": "无限批量大小(无限制)",
"provisioningKeysMaxBatchUnlimited": "无限制",
"provisioningKeysMaxBatchSizeInvalid": "输入一个有效的最大批处理大小(1-1,000,000)。",
@ -486,7 +480,7 @@
"provisioningKeysNeverUsed": "永不过期",
"provisioningKeysEdit": "编辑置备键",
"provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。",
"provisioningKeysApproveNewSites": "批准新点",
"provisioningKeysApproveNewSites": "批准新点",
"provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的节点。",
"provisioningKeysUpdateError": "更新预配键时出错",
"provisioningKeysUpdated": "置备密钥已更新",
@ -1403,7 +1397,6 @@
"createOrgUser": "创建组织用户",
"actionUpdateOrg": "更新组织",
"actionRemoveInvitation": "移除邀请",
"actionRemoveUserRole": "移除用户角色",
"actionUpdateUser": "更新用户",
"actionGetUser": "获取用户",
"actionGetOrgUser": "获取组织用户",
@ -1426,8 +1419,6 @@
"setupTokenDescription": "从服务器控制台输入设置令牌。",
"setupTokenRequired": "需要设置令牌",
"actionUpdateSite": "更新站点",
"actionApproveSite": "批准站点",
"actionRejectSite": "拒绝站点",
"actionResetSiteBandwidth": "重置组织带宽",
"actionListSiteRoles": "允许站点角色列表",
"actionCreateResource": "创建资源",
@ -1525,7 +1516,6 @@
"otpAuthDescription": "从您的身份验证程序中输入代码或您的单次备份代码。",
"otpAuthSubmit": "提交代码",
"idpContinue": "或者继续",
"idpLastUsed": "上次使用",
"otpAuthBack": "回到密码",
"navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

View file

@ -21,7 +21,6 @@ export enum ActionsEnum {
getSite = "getSite",
listSites = "listSites",
updateSite = "updateSite",
updateSiteApprovals = "updateSiteApprovals",
restartSite = "restartSite",
resetSiteBandwidth = "resetSiteBandwidth",
reGenerateSecret = "reGenerateSecret",

View file

@ -200,10 +200,7 @@ export const resources = pgTable(
authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
authDaemonPort: integer("authDaemonPort").default(22123)
},
(t) => [
index("idx_resources_fulldomain")
@ -454,10 +451,7 @@ export const siteResources = pgTable(
onDelete: "set null"
}),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain"),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
fullDomain: varchar("fullDomain")
},
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
);

View file

@ -209,8 +209,7 @@ export const resources = sqliteTable("resources", {
authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: text("status").$type<"pending" | "approved">().default("approved")
authDaemonPort: integer("authDaemonPort").default(22123)
});
export const labels = sqliteTable("labels", {
@ -448,8 +447,7 @@ export const siteResources = sqliteTable("siteResources", {
onDelete: "set null"
}),
subdomain: text("subdomain"),
fullDomain: text("fullDomain"),
status: text("status").$type<"pending" | "approved">().default("approved")
fullDomain: text("fullDomain")
});
export const networks = sqliteTable("networks", {

View file

@ -34,6 +34,12 @@ import {
rebuildClientAssociationsFromSiteResource,
waitForSiteResourceRebuildIdle
} from "../rebuildClientAssociations";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
type ApplyBlueprintArgs = {
orgId: string;

View file

@ -26,6 +26,9 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
@ -198,19 +201,17 @@ export async function updatePrivateResources(
}
}
let resourceStatusFromSite: "approved" | "pending" = "approved";
if (siteId && allSites.length === 0) {
// only add if there are not provided sites
// Use the provided siteId directly, but verify it belongs to the org
const [siteSingle] = await trx
.select({ siteId: sites.siteId, status: sites.status })
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
if (siteSingle) {
allSites.push(siteSingle);
}
resourceStatusFromSite = siteSingle.status ?? "approved";
}
if (allSites.length === 0) {
@ -219,13 +220,6 @@ export async function updatePrivateResources(
);
}
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
if (existingResource) {
let domainInfo:
| { subdomain: string | null; domainId: string }
@ -249,7 +243,8 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
disableIcmp:
resourceData["disable-icmp"] ||
@ -268,8 +263,7 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
})
.where(
eq(
@ -502,7 +496,8 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
aliasAddress: aliasAddress,
disableIcmp:
@ -522,8 +517,7 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
})
.returning();

View file

@ -25,7 +25,6 @@ import {
rolePolicies,
roleResources,
roles,
Site,
sites,
Target,
TargetHealthCheck,
@ -75,40 +74,19 @@ export async function updatePublicResources(
)) {
const targetsToUpdate: Target[] = [];
const healthchecksToUpdate: TargetHealthCheck[] = [];
let resource: Resource;
let resourceStatusFromSite: "approved" | "pending" = "approved";
let providedSite: Partial<Site> | undefined;
if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[providedSite] = await trx
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
resourceStatusFromSite = providedSite?.status ?? "approved";
}
async function createTarget( // reusable function to create a target
resourceId: number,
targetData: TargetData
) {
const targetSiteId = targetData.site;
let site: Partial<Site> | undefined;
let site;
if (targetSiteId) {
// Look up site by niceId
[site] = await trx
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.select({ siteId: sites.siteId, type: sites.type })
.from(sites)
.where(
and(
@ -117,9 +95,15 @@ export async function updatePublicResources(
)
)
.limit(1);
} else if (siteId && providedSite) {
} else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
site = providedSite;
[site] = await trx
.select({ siteId: sites.siteId, type: sites.type })
.from(sites)
.where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
)
.limit(1);
} else {
throw new Error(`Target site is required`);
}
@ -155,7 +139,7 @@ export async function updatePublicResources(
.insert(targets)
.values({
resourceId: resourceId,
siteId: site.siteId!,
siteId: site.siteId,
ip: targetData.hostname,
mode: resourceData.mode as Target["mode"],
method: targetData.method,
@ -188,7 +172,7 @@ export async function updatePublicResources(
.insert(targetHealthCheck)
.values({
name: `${targetData.hostname}:${targetData.port}`,
siteId: site.siteId!,
siteId: site.siteId,
targetId: newTarget.targetId,
orgId: orgId,
hcEnabled: healthcheckData?.enabled || false,
@ -246,10 +230,7 @@ export async function updatePublicResources(
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
: resourceData.enabled;
const resourceSsl =
resourceData.ssl == undefined || resourceData.ssl == null
? true
@ -425,8 +406,7 @@ export async function updatePublicResources(
? (resourceData["proxy-protocol-version"] ??
1)
: 1,
resourcePolicyId: sharedPolicy.resourcePolicyId,
status: resourceStatusFromSite
resourcePolicyId: sharedPolicy.resourcePolicyId
})
.where(
eq(
@ -610,8 +590,7 @@ export async function updatePublicResources(
authDaemonPort:
resourceData["auth-daemon"]?.port || 22123,
resourcePolicyId: null,
defaultResourcePolicyId: inlinePolicyId,
status: resourceStatusFromSite
defaultResourcePolicyId: inlinePolicyId
})
.where(
eq(
@ -1152,7 +1131,6 @@ export async function updatePublicResources(
.values({
orgId,
niceId: resourceNiceId,
status: resourceStatusFromSite,
name: resourceData.name || "Unnamed Resource",
mode: resourceData.mode,
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(

View file

@ -470,7 +470,7 @@ export const PrivateResourceSchema = z
// proxyPort: z.int().positive().optional(),
"destination-port": z.int().positive().optional(),
destination: z.string().min(1).optional(),
enabled: z.boolean().default(true),
// enabled: z.boolean().default(true),
"tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"),
"disable-icmp": z.boolean().optional().default(false),

View file

@ -14,6 +14,8 @@ import {
} from "@server/db";
import logger from "@server/logger";
import { removeTargets } from "@server/routers/newt/targets";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export type DeleteResourceResult = {
deletedResource: Resource;
@ -115,10 +117,10 @@ export async function runResourceDeleteSideEffects(
.limit(1);
if (!site) {
logger.debug(
`Site with ID ${target.siteId} not found during resource delete side effects; skipping target removal`
throw createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${target.siteId} not found`
);
continue;
}
if (site.pubKey && site.type === "newt") {

View file

@ -1,7 +1,6 @@
import { and, eq, inArray, sql } from "drizzle-orm";
import { and, eq, sql } from "drizzle-orm";
import {
db,
resources,
siteNetworks,
siteResources,
targets,
@ -98,64 +97,6 @@ export function exceedsSiteAssociatedResourceDeleteLimit(
return resourceCount > MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE;
}
export async function getPendingResourceIdsForSite(
siteId: number,
trx: Transaction | typeof db = db
): Promise<number[]> {
const resourceIds = await getResourceIdsForSite(siteId, trx);
if (resourceIds.length === 0) {
return [];
}
const rows = await trx
.select({ resourceId: resources.resourceId })
.from(resources)
.where(
and(
inArray(resources.resourceId, resourceIds),
eq(resources.status, "pending")
)
);
return rows.map((row) => row.resourceId);
}
export async function getPendingSiteResourceIdsForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<number[]> {
const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
if (siteResourceIds.length === 0) {
return [];
}
const rows = await trx
.select({ siteResourceId: siteResources.siteResourceId })
.from(siteResources)
.where(
and(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.status, "pending")
)
);
return rows.map((row) => row.siteResourceId);
}
export async function getPendingAssociatedResourceCountForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<number> {
const [resourceIds, siteResourceIds] = await Promise.all([
getPendingResourceIdsForSite(siteId, trx),
getPendingSiteResourceIdsForSite(siteId, orgId, trx)
]);
return resourceIds.length + siteResourceIds.length;
}
export async function deleteAssociatedResourcesForSite(
siteId: number,
orgId: string,
@ -164,32 +105,12 @@ export async function deleteAssociatedResourcesForSite(
const resourceIds = await getResourceIdsForSite(siteId, trx);
const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
const [deletedResources, siteResourcesDeleted] = await Promise.all([
const [resources, siteResourcesDeleted] = await Promise.all([
performDeleteResources(resourceIds, trx),
performDeleteSiteResources(siteResourceIds, trx)
]);
return { resources: deletedResources, siteResources: siteResourcesDeleted };
}
export async function deletePendingAssociatedResourcesForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<DeleteSiteAssociatedResourcesSideEffects> {
const resourceIds = await getPendingResourceIdsForSite(siteId, trx);
const siteResourceIds = await getPendingSiteResourceIdsForSite(
siteId,
orgId,
trx
);
const [deletedResources, siteResourcesDeleted] = await Promise.all([
performDeleteResources(resourceIds, trx),
performDeleteSiteResources(siteResourceIds, trx)
]);
return { resources: deletedResources, siteResources: siteResourcesDeleted };
return { resources, siteResources: siteResourcesDeleted };
}
export async function runDeleteSiteAssociatedResourcesSideEffects(

View file

@ -496,7 +496,6 @@ export function generateRemoteSubnets(
): string[] {
const remoteSubnets = allSiteResources
.filter((sr) => {
if (!sr.enabled) return false;
if (!sr.destination) return false;
if (sr.mode === "cidr") {
@ -531,7 +530,6 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
return allSiteResources
.filter(
(sr) =>
sr.enabled &&
sr.aliasAddress &&
((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) ||
(sr.fullDomain && sr.mode == "http"))
@ -664,13 +662,6 @@ export async function generateSubnetProxyTargetV2(
subnet: string | null;
}[]
): Promise<SubnetProxyTargetV2[] | undefined> {
if (!siteResource.enabled) {
logger.debug(
`Site resource ${siteResource.siteResourceId} is disabled, skipping target generation.`
);
return;
}
if (clients.length === 0) {
logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`

View file

@ -1561,19 +1561,9 @@ export async function handleMessagingForUpdatedSiteResource(
updatedSiteResource.udpPortRangeString ||
existingSiteResource.disableIcmp !==
updatedSiteResource.disableIcmp);
// Toggling enabled on/off doesn't change any of the fields above, but it
// does change whether targets/peer data should exist at all, so it needs
// to drive the same old->new diff machinery: going enabled->disabled
// diffs "real data" against "nothing" (a remove), and disabled->enabled
// diffs "nothing" against "real data" (an add). generateSubnetProxyTargetV2/
// generateRemoteSubnets/generateAliasConfig already return nothing for a
// disabled resource, so no other changes are needed here.
const enabledChanged =
existingSiteResource &&
existingSiteResource.enabled !== updatedSiteResource.enabled;
logger.debug(
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)} enabledChanged=${Boolean(enabledChanged)}`
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)}`
);
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
@ -1584,16 +1574,14 @@ export async function handleMessagingForUpdatedSiteResource(
fullDomainChanged ||
sslChanged ||
portRangesChanged ||
destinationPortChanged ||
enabledChanged
destinationPortChanged
) {
const shouldUpdateTargets =
destinationChanged ||
sslChanged ||
portRangesChanged ||
fullDomainChanged ||
destinationPortChanged ||
enabledChanged;
destinationPortChanged;
logger.debug(
`handleMessagingForUpdatedSiteResource: entering unchanged-site update path shouldUpdateTargets=${shouldUpdateTargets}`
@ -1669,22 +1657,20 @@ export async function handleMessagingForUpdatedSiteResource(
peerDataUpdateBatch.push({
clientId: client.clientId,
siteId,
remoteSubnets:
destinationChanged || enabledChanged
? {
oldRemoteSubnets:
!oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
remoteSubnets: destinationChanged
? {
oldRemoteSubnets: !oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
aliases:
aliasChanged || fullDomainChanged || enabledChanged // the full domain is sent down as an alias
aliasChanged || fullDomainChanged // the full domain is sent down as an alias
? {
oldAliases: generateAliasConfig([
existingSiteResource

View file

@ -248,22 +248,6 @@ authenticated.post(
site.updateSite
);
authenticated.post(
"/site/:siteId/approve",
verifySiteAccess,
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
logActionAudit(ActionsEnum.updateSiteApprovals),
site.approveSite
);
authenticated.post(
"/site/:siteId/reject",
verifySiteAccess,
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
logActionAudit(ActionsEnum.updateSiteApprovals),
site.rejectSite
);
authenticated.delete(
"/site/:siteId",
verifySiteAccess,

View file

@ -138,7 +138,6 @@ authenticated.post(
logActionAudit(ActionsEnum.updateSite),
site.updateSite
);
authenticated.post(
"/org/:orgId/reset-bandwidth",
verifyApiKeyOrgAccess,

View file

@ -157,8 +157,7 @@ async function resolveAccessibleIdsUncached(
.where(
and(
eq(userResources.userId, userId),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
),
userRoleIds.length > 0
@ -172,8 +171,7 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(roleResources.roleId, userRoleIds),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
)
: Promise.resolve([]),
@ -185,11 +183,7 @@ async function resolveAccessibleIdsUncached(
eq(effectiveResourcePolicyId, userPolicies.resourcePolicyId)
)
.where(
and(
eq(userPolicies.userId, userId),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
and(eq(userPolicies.userId, userId), eq(resources.orgId, orgId))
),
userRoleIds.length > 0
? db
@ -205,48 +199,21 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(rolePolicies.roleId, userRoleIds),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
)
: Promise.resolve([]),
db
.select({ siteResourceId: userSiteResources.siteResourceId })
.from(userSiteResources)
.innerJoin(
siteResources,
eq(
userSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
eq(userSiteResources.userId, userId),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
),
.where(eq(userSiteResources.userId, userId)),
userRoleIds.length > 0
? db
.select({
siteResourceId: roleSiteResources.siteResourceId
})
.from(roleSiteResources)
.innerJoin(
siteResources,
eq(
roleSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
inArray(roleSiteResources.roleId, userRoleIds),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
)
.where(inArray(roleSiteResources.roleId, userRoleIds))
: Promise.resolve([])
]);
@ -398,7 +365,6 @@ async function filterPublicResourceIdsByTextSearch(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
textMatch
)
);
@ -436,7 +402,6 @@ async function filterSiteResourceIdsByTextSearch(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
textMatch
)
);
@ -538,8 +503,7 @@ async function listSiteGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (searchPublic) {
publicConditions.push(searchPublic);
@ -594,8 +558,7 @@ async function listSiteGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (searchSite) {
siteConditions.push(searchSite);
@ -658,8 +621,7 @@ async function listSiteGroups(
const noSitePublicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (searchPublic) {
noSitePublicConditions.push(searchPublic);
@ -693,8 +655,7 @@ async function listSiteGroups(
const noSiteSiteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (searchSite) {
noSiteSiteConditions.push(searchSite);
@ -785,8 +746,7 @@ async function listLabelGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
const searchPublic = buildSearchConditionForPublic(query.query);
if (searchPublic) {
@ -850,8 +810,7 @@ async function listLabelGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
const searchSite = buildSearchConditionForSiteResource(query.query);
if (searchSite) {
@ -1038,7 +997,6 @@ async function mapPublicResources(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@ -1130,7 +1088,6 @@ async function mapSiteResources(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@ -1425,8 +1382,7 @@ async function collectAccessibleSites(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (siteNameSearch) {
publicConditions.push(siteNameSearch);
@ -1466,8 +1422,7 @@ async function collectAccessibleSites(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (siteNameSearch) {
siteConditions.push(siteNameSearch);
@ -1521,7 +1476,6 @@ async function collectAccessibleLabels(
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {
@ -1557,7 +1511,6 @@ async function collectAccessibleLabels(
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {

View file

@ -148,12 +148,7 @@ export async function buildClientConfigurationForNewtClient(
.from(siteResources)
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
and(
eq(siteNetworks.siteId, siteId),
eq(siteResources.enabled, true)
)
)
.where(eq(siteNetworks.siteId, siteId))
.then((rows) => rows.map((r) => r.siteResources));
const targetsToSend: SubnetProxyTargetV2[] = [];

View file

@ -16,7 +16,7 @@ import {
generateRemoteSubnets
} from "@server/lib/ip";
import logger from "@server/logger";
import { and, eq, inArray } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import config from "@server/lib/config";
@ -70,13 +70,7 @@ export async function buildSiteConfigurationForOlmClient(
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
and(
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
),
eq(siteResources.enabled, true)
)
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
);
const siteResourcesBySiteId = new Map<number, SiteResource[]>();

View file

@ -138,15 +138,6 @@ const listResourcesSchema = z.strictObject({
description:
"When set, only resources that have at least one target on this site are returned"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by resource status"
}),
labels: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
@ -460,7 +451,6 @@ export async function listResources(
sort_by,
order,
siteId,
status,
labels: labelFilter
} = parsedQuery.data;
@ -670,10 +660,6 @@ export async function listResources(
}
}
if (typeof status !== "undefined") {
conditions.push(eq(resources.status, status));
}
if (siteId != null) {
const resourcesWithSite = db
.select({ resourceId: targets.resourceId })

View file

@ -45,19 +45,18 @@ function userResourceAliasesCacheKey(
page: number,
pageSize: number,
includeLabels: boolean,
labelFilter: string[],
status?: "pending" | "approved"
labelFilter: string[]
) {
const labelsKey =
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}:${status ?? "all"}`;
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
}
const listUserResourceAliasesParamsSchema = z.strictObject({
orgId: z.string()
});
const listUserResourceAliasesQuerySchema = z.object({
const listUserResourceAliasesQuerySchema = z.strictObject({
pageSize: z.coerce
.number<string>()
.int()
@ -97,16 +96,7 @@ const listUserResourceAliasesQuerySchema = z.object({
type: "array",
description:
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
})
})
});
export type UserResourceAliasItem = {
@ -140,8 +130,7 @@ export async function listUserResourceAliases(
page,
pageSize,
includeLabels,
labels: labelFilter,
status
labels: labelFilter
} = parsedQuery.data;
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
@ -183,8 +172,7 @@ export async function listUserResourceAliases(
page,
pageSize,
includeLabels,
labelFilter ?? [],
status
labelFilter ?? []
);
const cachedData: ListUserResourceAliasesResponse | undefined =
await cache.get(cacheKey);
@ -269,10 +257,6 @@ export async function listUserResourceAliases(
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
];
if (typeof status !== "undefined") {
whereConditions.push(eq(siteResources.status, status));
}
if (labelFilter && labelFilter.length > 0) {
whereConditions.push(
inArray(

View file

@ -1,251 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
resources,
siteNetworks,
siteResources,
sites,
type Site,
type SiteResource
} from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import {
getResourceIdsForSite,
getSiteResourceIdsForSite
} from "@server/lib/deleteSiteAssociatedResources";
import {
handleMessagingForUpdatedSiteResource,
rebuildClientAssociationsFromSiteResource,
waitForSiteResourceRebuildIdle
} from "@server/lib/rebuildClientAssociations";
const approveSiteParamsSchema = z.strictObject({
siteId: z.coerce.number().int().positive()
});
registry.registerPath({
method: "post",
path: "/site/{siteId}/approve",
description:
"Approve a pending site and approve (and enable when needed) its associated resources.",
tags: [OpenAPITags.Site],
request: {
params: approveSiteParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
type SiteResourceEnableSideEffect = {
existing: SiteResource;
updated: SiteResource;
siteIds: number[];
};
export async function approveSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = approveSiteParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteId } = parsedParams.data;
const [existingSite] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!existingSite) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
if (!existingSite.orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Site with ID ${siteId} has no organization`
)
);
}
const orgId = existingSite.orgId;
let updatedSite: Site | undefined;
const siteResourceEnableSideEffects: SiteResourceEnableSideEffect[] =
[];
await db.transaction(async (trx) => {
[updatedSite] = await trx
.update(sites)
.set({ status: "approved" })
.where(eq(sites.siteId, siteId))
.returning();
const resourceIds = await getResourceIdsForSite(siteId, trx);
const siteResourceIds = await getSiteResourceIdsForSite(
siteId,
orgId,
trx
);
if (resourceIds.length > 0) {
const pendingDisabledResources = await trx
.select({ resourceId: resources.resourceId })
.from(resources)
.where(
and(
inArray(resources.resourceId, resourceIds),
eq(resources.status, "pending"),
eq(resources.enabled, false)
)
);
await trx
.update(resources)
.set({ status: "approved" })
.where(inArray(resources.resourceId, resourceIds));
if (pendingDisabledResources.length > 0) {
await trx
.update(resources)
.set({ enabled: true })
.where(
inArray(
resources.resourceId,
pendingDisabledResources.map(
(r) => r.resourceId
)
)
);
}
}
if (siteResourceIds.length > 0) {
const existingSiteResources = await trx
.select()
.from(siteResources)
.where(
inArray(siteResources.siteResourceId, siteResourceIds)
);
const pendingDisabledSiteResources =
existingSiteResources.filter(
(sr) => sr.status === "pending" && !sr.enabled
);
await trx
.update(siteResources)
.set({ status: "approved" })
.where(
inArray(siteResources.siteResourceId, siteResourceIds)
);
if (pendingDisabledSiteResources.length > 0) {
const enableIds = pendingDisabledSiteResources.map(
(sr) => sr.siteResourceId
);
const updatedEnabledSiteResources = await trx
.update(siteResources)
.set({ enabled: true })
.where(inArray(siteResources.siteResourceId, enableIds))
.returning();
for (const updated of updatedEnabledSiteResources) {
const existing = pendingDisabledSiteResources.find(
(sr) => sr.siteResourceId === updated.siteResourceId
);
if (!existing || !updated.networkId) {
continue;
}
const networkSites = await trx
.select({ siteId: siteNetworks.siteId })
.from(siteNetworks)
.where(
eq(siteNetworks.networkId, updated.networkId)
);
siteResourceEnableSideEffects.push({
existing,
updated,
siteIds: networkSites.map((s) => s.siteId)
});
}
}
}
});
for (const sideEffect of siteResourceEnableSideEffects) {
rebuildClientAssociationsFromSiteResource(sideEffect.updated)
.then(() =>
waitForSiteResourceRebuildIdle(
sideEffect.updated.siteResourceId
)
)
.then(() =>
handleMessagingForUpdatedSiteResource(
sideEffect.existing,
sideEffect.updated,
sideEffect.siteIds,
sideEffect.siteIds
)
)
.catch((e) => {
logger.error(
`Failed to rebuild and handle messaging for site resource ${sideEffect.updated.siteResourceId} after site approval:`,
e
);
});
}
return response(res, {
data: updatedSite ?? null,
success: true,
error: false,
message: "Site approved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -159,39 +159,15 @@ export async function deleteSite(
siteResources: []
};
if (deleteResources) {
await db.transaction(async (trx) => {
await db.transaction(async (trx) => {
if (deleteResources) {
resourceSideEffects = await deleteAssociatedResourcesForSite(
siteId,
site.orgId,
trx
);
}
if (resourceSideEffects.resources.length > 0) {
await usageService.add(
site.orgId,
LimitId.PUBLIC_RESOURCES,
-resourceSideEffects.resources.length,
trx
);
}
if (resourceSideEffects.siteResources.length > 0) {
await usageService.add(
site.orgId,
LimitId.PRIVATE_RESOURCES,
-resourceSideEffects.siteResources.length,
trx
);
}
});
await runDeleteSiteAssociatedResourcesSideEffects(
resourceSideEffects
);
}
await db.transaction(async (trx) => {
if (site.type == "wireguard") {
if (site.pubKey) {
await deletePeer(site.exitNodeId!, site.pubKey);
@ -204,6 +180,12 @@ export async function deleteSite(
await usageService.add(site.orgId, LimitId.SITES, -1, trx);
});
if (deleteResources) {
await runDeleteSiteAssociatedResourcesSideEffects(
resourceSideEffects
);
}
if (deletedNewt) {
const payload = {
type: `newt/wg/terminate`,

View file

@ -3,8 +3,6 @@ export * from "./getStatusHistory";
export * from "./createSite";
export * from "./deleteSite";
export * from "./updateSite";
export * from "./approveSite";
export * from "./rejectSite";
export * from "./listSites";
export * from "./listSiteRoles";
export * from "./pickSiteDefaults";

View file

@ -1,196 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { deletePeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error";
import { sendToClient } from "#dynamic/routers/ws";
import { OpenAPITags, registry } from "@server/openApi";
import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations";
import { usageService } from "@server/lib/billing/usageService";
import { LimitId } from "@server/lib/billing";
import {
deletePendingAssociatedResourcesForSite,
exceedsSiteAssociatedResourceDeleteLimit,
getPendingAssociatedResourceCountForSite,
runDeleteSiteAssociatedResourcesSideEffects,
MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE,
type DeleteSiteAssociatedResourcesSideEffects
} from "@server/lib/deleteSiteAssociatedResources";
const rejectSiteParamsSchema = z.strictObject({
siteId: z.coerce.number().int().positive()
});
registry.registerPath({
method: "post",
path: "/site/{siteId}/reject",
description:
"Reject a pending site by deleting it and any associated resources that are still pending.",
tags: [OpenAPITags.Site],
request: {
params: rejectSiteParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
export async function rejectSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = rejectSiteParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteId } = parsedParams.data;
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
if (!site.orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Site with ID ${siteId} has no organization`
)
);
}
const pendingAssociatedResourceCount =
await getPendingAssociatedResourceCountForSite(siteId, site.orgId);
if (
exceedsSiteAssociatedResourceDeleteLimit(
pendingAssociatedResourceCount
)
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Cannot reject site and associated pending resources when the site has more than ${MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE} pending resources`
)
);
}
const [deletedNewt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId))
.limit(1);
let resourceSideEffects: DeleteSiteAssociatedResourcesSideEffects = {
resources: [],
siteResources: []
};
await db.transaction(async (trx) => {
resourceSideEffects = await deletePendingAssociatedResourcesForSite(
siteId,
site.orgId,
trx
);
if (resourceSideEffects.resources.length > 0) {
await usageService.add(
site.orgId,
LimitId.PUBLIC_RESOURCES,
-resourceSideEffects.resources.length,
trx
);
}
if (resourceSideEffects.siteResources.length > 0) {
await usageService.add(
site.orgId,
LimitId.PRIVATE_RESOURCES,
-resourceSideEffects.siteResources.length,
trx
);
}
});
await runDeleteSiteAssociatedResourcesSideEffects(resourceSideEffects);
await db.transaction(async (trx) => {
if (site.type == "wireguard") {
if (site.pubKey) {
await deletePeer(site.exitNodeId!, site.pubKey);
}
} else if (site.type == "newt") {
await cleanupSiteAssociations(site, trx);
}
await trx.delete(sites).where(eq(sites.siteId, siteId));
await usageService.add(site.orgId, LimitId.SITES, -1, trx);
});
if (deletedNewt) {
const payload = {
type: `newt/wg/terminate`,
data: {}
};
sendToClient(deletedNewt.newtId, payload).catch((error) => {
logger.error(
"Failed to send termination message to newt:",
error
);
});
}
return response(res, {
data: null,
success: true,
error: false,
message: "Site rejected successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
if (createHttpError.isHttpError(error)) {
return next(error);
}
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -21,6 +21,7 @@ const updateSiteBodySchema = z
name: z.string().min(1).max(255).optional(),
niceId: z.string().min(1).max(255).optional(),
dockerSocketEnabled: z.boolean().optional(),
status: z.enum(["pending", "approved"]).optional(),
autoUpdateEnabled: z.boolean().optional(),
autoUpdateOverrideOrg: z.boolean().optional()
})

View file

@ -56,6 +56,7 @@ const createSiteResourceSchema = z
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
destinationPort: z.int().positive().optional(),
destination: z.string().min(1).nullish(),
enabled: z.boolean().default(true),
alias: z
.string()
.regex(
@ -274,6 +275,7 @@ export async function createSiteResource(
scheme,
destinationPort,
destination,
enabled,
ssl,
alias,
userIds,
@ -537,6 +539,7 @@ export async function createSiteResource(
destination: destination, // the ssh can be null
scheme,
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,

View file

@ -86,15 +86,6 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
description:
"When set, only site resources associated with this site (via network) are returned"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
}),
labels: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
@ -292,7 +283,6 @@ export async function listAllSiteResourcesByOrg(
sort_by,
order,
siteId,
status,
labels: labelFilter
} = parsedQuery.data;
@ -325,10 +315,6 @@ export async function listAllSiteResourcesByOrg(
conditions.push(eq(siteResources.mode, mode));
}
if (typeof status !== "undefined") {
conditions.push(eq(siteResources.status, status));
}
if (labelFilter && labelFilter.length > 0) {
conditions.push(
inArray(

View file

@ -47,15 +47,6 @@ const listSiteResourcesQuerySchema = z.strictObject({
enum: ["asc", "desc"],
default: "asc",
description: "Sort order"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
})
});
@ -119,7 +110,7 @@ export async function listSiteResources(
}
const { siteId, orgId } = parsedParams.data;
const { limit, offset, sort_by, order, status } = parsedQuery.data;
const { limit, offset, sort_by, order } = parsedQuery.data;
// Verify the site exists and belongs to the org
const site = await db
@ -133,15 +124,6 @@ export async function listSiteResources(
}
// Get site resources by joining networks to siteResources via siteNetworks
const conditions = [
eq(siteNetworks.siteId, siteId),
eq(siteResources.orgId, orgId)
];
if (typeof status !== "undefined") {
conditions.push(eq(siteResources.status, status));
}
const siteResourcesList = await db
.select()
.from(siteNetworks)
@ -150,7 +132,12 @@ export async function listSiteResources(
siteResources,
eq(siteResources.networkId, networks.networkId)
)
.where(and(...conditions))
.where(
and(
eq(siteNetworks.siteId, siteId),
eq(siteResources.orgId, orgId)
)
)
.orderBy(
sort_by
? order === "asc"

View file

@ -152,11 +152,6 @@ const updateSiteResourceSchema = z
)
.refine(
(data) => {
// this is a partial update; only enforce destination when the
// caller is actually changing mode or destination
if (data.mode === undefined && data.destination === undefined) {
return true;
}
// destination is only optional for ssh mode with native authDaemonMode
if (data.mode === "ssh" && data.authDaemonMode === "native") {
return true;
@ -416,10 +411,8 @@ export async function updateSiteResource(
: [];
const existingSiteIds = existingSiteNetworks.map((sn) => sn.siteId);
// undefined means "leave unchanged" (partial update); only nulled out
// when the mode is explicitly being changed away from http
let fullDomain: string | null | undefined = undefined;
let finalSubdomain: string | null | undefined = undefined;
let fullDomain: string | null = null;
let finalSubdomain: string | null = null;
if (domainId) {
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
@ -455,11 +448,6 @@ export async function updateSiteResource(
)
);
}
} else if (mode !== undefined && mode !== "http") {
// mode is explicitly changing away from http, so the resource
// can no longer have a domain associated with it
fullDomain = null;
finalSubdomain = null;
}
// make sure the alias is unique within the org if provided
@ -528,28 +516,15 @@ export async function updateSiteResource(
destination: destination,
destinationPort: destinationPort,
enabled: enabled,
alias:
alias !== undefined
? alias
? alias.trim()
: null
: mode !== undefined &&
mode !== "host" &&
mode !== "ssh"
? null
: undefined,
alias: alias ? alias.trim() : null,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh"
? ""
: udpPortRangeString,
disableIcmp:
mode !== undefined
? disableIcmp ||
(mode == "http" || mode == "ssh"
? true
: false)
: disableIcmp,
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false),
domainId,
subdomain: finalSubdomain,
fullDomain,

View file

@ -23,7 +23,7 @@ import {
import { ChevronsUpDown } from "lucide-react";
import { useTranslations } from "next-intl";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { PrivateResourceMultiSiteRoutingHelp } from "@app/components/PrivateResourceMultiSiteRoutingHelp";
import { PrivateResourceMultiSiteRoutingHelp } from "./PrivateResourceMultiSiteRoutingHelp";
type PrivateResourceSitesFieldProps<T extends FieldValues> = {
control: Control<T>;

View file

@ -9,10 +9,10 @@ import {
} from "@app/components/Settings";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { SshServerSettingsFields } from "@app/components/SshServerSettingsFields";
import { PrivateResourceAliasField } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceAliasField } from "./PrivateResourceDestinationFields";
import { PrivateResourceSitesField } from "./PrivateResourceSitesField";
import { getSshUseMultiSiteTargetForm } from "./privateResourceUtils";
import { inferSshPamMode } from "@app/lib/privateResourceForm";
import { getSshUseMultiSiteTargetForm } from "@app/lib/privateResourceUtils";
import {
FormControl,
FormField,

View file

@ -29,7 +29,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useActionState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { PrivateResourceAccessFields } from "@app/components/PrivateResourceAccessFields";
import { PrivateResourceAccessFields } from "../../PrivateResourceAccessFields";
export default function PrivateResourceAccessPage() {
const t = useTranslations();

View file

@ -20,12 +20,12 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceCidrDestinationField } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { asAnyControl, asAnySetValue } from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceCidrDestinationField } from "../../PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "../../PrivateResourcePortRanges";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import { asAnyControl, asAnySetValue } from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceCidrPage() {
const t = useTranslations();

View file

@ -16,21 +16,19 @@ import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { SwitchInput } from "@app/components/SwitchInput";
import { createGeneralFormSchema } from "@app/lib/privateResourceForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useActionState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceGeneralPage() {
const t = useTranslations();
@ -43,8 +41,7 @@ export default function PrivateResourceGeneralPage() {
resolver: zodResolver(formSchema),
defaultValues: {
name: siteResource.name,
niceId: siteResource.niceId,
enabled: siteResource.enabled
niceId: siteResource.niceId
}
});
@ -55,8 +52,7 @@ export default function PrivateResourceGeneralPage() {
const data = form.getValues();
await save({
name: data.name,
niceId: data.niceId,
enabled: data.enabled
niceId: data.niceId
});
}, null);
@ -80,42 +76,6 @@ export default function PrivateResourceGeneralPage() {
id="private-resource-general-form"
>
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="enabled"
render={() => (
<FormItem>
<FormControl>
<SwitchInput
id="enable-resource"
defaultChecked={
siteResource.enabled
}
label={t(
"resourceEnable"
)}
onCheckedChange={(
val
) =>
form.setValue(
"enabled",
val
)
}
/>
</FormControl>
<FormDescription>
{t(
"disabledResourceDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}

View file

@ -20,16 +20,16 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHostDestinationFields } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceHostDestinationFields } from "../../PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "../../PrivateResourcePortRanges";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceHostPage() {
const t = useTranslations();

View file

@ -22,15 +22,15 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "@app/components/PrivateResourceHttpFields";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "../../PrivateResourceHttpFields";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceHttpPage() {
const t = useTranslations();

View file

@ -26,15 +26,15 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSshFields } from "@app/components/PrivateResourceSshFields";
import type { Selectedsite } from "@app/components/site-selector";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSshFields } from "../../PrivateResourceSshFields";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
import type { Selectedsite } from "@app/components/site-selector";
export default function PrivateResourceSshPage() {
const t = useTranslations();

View file

@ -50,20 +50,16 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "@app/components/PrivateResourceHttpFields";
import { PrivateResourceSshFields } from "@app/components/PrivateResourceSshFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { PrivateResourceSitesField } from "../PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "../PrivateResourceHttpFields";
import { PrivateResourceSshFields } from "../PrivateResourceSshFields";
import { PrivateResourcePortRanges } from "../PrivateResourcePortRanges";
import {
PrivateResourceAliasField,
PrivateResourceCidrDestinationField,
PrivateResourceHostDestinationFields
} from "@app/components/PrivateResourceDestinationFields";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
} from "../PrivateResourceDestinationFields";
import { asAnyControl, asAnySetValue, asAnyWatch } from "../formControlUtils";
export default function CreatePrivateResourcePage() {
const params = useParams();

View file

@ -27,7 +27,6 @@ export default async function ClientResourcesPage(
const params = await props.params;
const t = await getTranslations();
const searchParams = new URLSearchParams(await props.searchParams);
searchParams.set("status", "approved");
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
let pagination: ListAllSiteResourcesByOrgResponse["pagination"] = {

View file

@ -38,7 +38,6 @@ export default async function ProxyResourcesPage(
const params = await props.params;
const t = await getTranslations();
const searchParams = new URLSearchParams(await props.searchParams);
searchParams.set("status", "approved");
let resources: ListResourcesResponse["resources"] = [];
let pagination: ListResourcesResponse["pagination"] = {

View file

@ -16,11 +16,8 @@ import LoginCardHeader from "@app/components/LoginCardHeader";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { LoginFormIDP } from "@app/components/LoginForm";
import { ListIdpsResponse, type GetIdpResponse } from "@server/routers/idp";
import { ListIdpsResponse } from "@server/routers/idp";
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
import z from "zod";
export const metadata: Metadata = {
title: "Log In"
@ -32,9 +29,8 @@ export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const user = await verifySession({ skipCheckVerifyEmail: true });
const lastUsedIdpCookie = (await cookies()).get(LAST_USED_IDP_COOKIE_NAME);
const getUser = cache(verifySession);
const user = await getUser({ skipCheckVerifyEmail: true });
const isInvite = searchParams?.redirect?.includes("/invite");
const forceLoginParam = searchParams?.forceLogin;
@ -89,47 +85,19 @@ export default async function Page(props: {
(build === "enterprise" && env.app.identityProviderMode === "org");
let loginIdps: LoginFormIDP[] = [];
let lastUsedIdpForSmartLogin: (LoginFormIDP & { orgId?: string }) | null =
null;
if (!useSmartLogin) {
// Load IdPs for DashboardLoginForm (OSS or org-only IdP mode)
if (build === "oss" || env.app.identityProviderMode !== "org") {
const idpsRes =
await priv.get<AxiosResponse<ListIdpsResponse>>("/idp");
const idpsRes = await cache(
async () =>
await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
)();
loginIdps = idpsRes.data.data.idps.map((idp) => ({
idpId: idp.idpId,
name: idp.name,
variant: idp.type
})) as LoginFormIDP[];
}
} else {
if (lastUsedIdpCookie) {
const lastUsedIdpSchema = z.object({
orgId: z.string().optional(),
idpId: z.number()
});
try {
const persistedData = lastUsedIdpSchema.parse(
JSON.parse(lastUsedIdpCookie.value)
);
const idpRes = await priv.get<AxiosResponse<GetIdpResponse>>(
`/idp/${persistedData.idpId}`
);
const idp = idpRes.data.data.idp;
lastUsedIdpForSmartLogin = {
idpId: idp.idpId,
name: idp.name,
variant: idp.type,
orgId: persistedData.orgId,
lastUsed: true
};
} catch (error) {
// the idp might not exist or the data is malformatted, skip this
}
}
}
const t = await getTranslations();
@ -192,7 +160,6 @@ export default async function Page(props: {
redirect={redirectUrl}
forceLogin={forceLogin}
defaultUser={defaultUser}
lastUsedIdp={lastUsedIdpForSmartLogin}
orgSignIn={
!isInvite &&
(build === "saas" ||

View file

@ -13,8 +13,6 @@ import { redirect } from "next/navigation";
import OrgLoginPage from "@app/components/OrgLoginPage";
import { pullEnv } from "@app/lib/pullEnv";
import type { Metadata } from "next";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
export const metadata: Metadata = {
title: "Organization Login"
@ -70,22 +68,15 @@ export default async function OrgAuthPage(props: {
variant: idp.variant
})) as LoginFormIDP[];
const hasLoginPageBranding = await isOrgSubscribed(
orgId,
tierMatrix.loginPageBranding
);
let branding: LoadLoginPageBrandingResponse | null = null;
if (hasLoginPageBranding) {
try {
const res = await priv.get<
AxiosResponse<LoadLoginPageBrandingResponse>
>(`/login-page-branding?orgId=${orgId}`);
if (res.status === 200) {
branding = res.data.data;
}
} catch (error) {}
}
try {
const res = await priv.get<
AxiosResponse<LoadLoginPageBrandingResponse>
>(`/login-page-branding?orgId=${orgId}`);
if (res.status === 200) {
branding = res.data.data;
}
} catch (error) {}
return (
<OrgLoginPage

View file

@ -19,7 +19,6 @@ import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
import { OrgSelectionForm } from "@app/components/OrgSelectionForm";
import OrgLoginPage from "@app/components/OrgLoginPage";
import type { Metadata } from "next";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export const metadata: Metadata = {
title: "Choose Organization"
@ -84,10 +83,7 @@ export default async function OrgAuthPage(props: {
redirect(env.app.dashboardUrl);
}
const subscribed = await isOrgSubscribed(
loginPage.orgId,
tierMatrix.loginPageDomain
);
const subscribed = await isOrgSubscribed(loginPage.orgId);
if (build === "saas" && !subscribed) {
console.log(

View file

@ -27,7 +27,6 @@ import { CheckOrgUserAccessResponse } from "@server/routers/org";
import OrgPolicyRequired from "@app/components/OrgPolicyRequired";
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { Metadata } from "next";
export const metadata: Metadata = {
@ -71,25 +70,14 @@ export default async function ResourceAuthPage(props: {
);
}
const hasLoginPageDomain = await isOrgSubscribed(
authInfo.orgId,
tierMatrix.loginPageDomain
);
const hasOrgOidc = await isOrgSubscribed(
authInfo.orgId,
tierMatrix.orgOidc
);
const hasLoginPageBranding = await isOrgSubscribed(
authInfo.orgId,
tierMatrix.loginPageBranding
);
const subscribed = await isOrgSubscribed(authInfo.orgId);
const allHeaders = await headers();
const host = allHeaders.get("host");
const expectedHost = env.app.dashboardUrl.split("//")[1];
if (host !== expectedHost) {
if (build === "saas" && !hasLoginPageDomain) {
if (build === "saas" && !subscribed) {
redirect(env.app.dashboardUrl);
}
@ -118,10 +106,7 @@ export default async function ResourceAuthPage(props: {
const redirectPort = new URL(searchParams.redirect).port;
const serverResourceHostWithPort = `${serverResourceHost}:${redirectPort}`;
const wildcardMatchesRedirect = (
wildcardDomain: string,
host: string
): boolean => {
const wildcardMatchesRedirect = (wildcardDomain: string, host: string): boolean => {
if (!wildcardDomain.startsWith("*.")) return false;
const suffix = wildcardDomain.slice(1); // e.g. ".wildcard.owen.fosrl.io"
return host.endsWith(suffix) && host.length > suffix.length;
@ -243,7 +228,7 @@ export default async function ResourceAuthPage(props: {
let loginIdps: LoginFormIDP[] = [];
if (build === "saas" || env.app.identityProviderMode === "org") {
if (hasOrgOidc) {
if (subscribed) {
const idpsRes = await cache(
async () =>
await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
@ -286,7 +271,7 @@ export default async function ResourceAuthPage(props: {
let branding: LoadLoginPageBrandingResponse | null = null;
try {
if (hasLoginPageBranding) {
if (subscribed) {
const res = await priv.get<
AxiosResponse<LoadLoginPageBrandingResponse>
>(`/login-page-branding?orgId=${authInfo.orgId}`);

View file

@ -5,6 +5,7 @@ import UserProvider from "@app/providers/UserProvider";
import { ListUserOrgsResponse } from "@server/routers/org";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { cache } from "react";
import OrganizationLanding from "@app/components/OrganizationLanding";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
@ -12,6 +13,7 @@ import { Layout } from "@app/components/Layout";
import RedirectToOrg from "@app/components/RedirectToOrg";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { cookies } from "next/headers";
import { build } from "@server/build";
export const dynamic = "force-dynamic";
@ -27,7 +29,8 @@ export default async function Page(props: {
const env = pullEnv();
const user = await verifySession({ skipCheckVerifyEmail: true });
const getUser = cache(verifySession);
const user = await getUser({ skipCheckVerifyEmail: true });
let complete = false;
try {

View file

@ -1,25 +1,26 @@
"use client";
import { generateOidcUrlProxy } from "@app/actions/server";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useEffect, useState } from "react";
import { Button } from "@app/components/ui/button";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
import { setClientCookie } from "@app/lib/setClientCookie";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useTranslations } from "next-intl";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
import {
generateOidcUrlProxy,
type GenerateOidcUrlResponse
} from "@app/actions/server";
import {
redirect as redirectTo,
useRouter,
useParams,
useSearchParams
} from "next/navigation";
import { useEffect, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { cleanRedirect } from "@app/lib/cleanRedirect";
export type LoginFormIDP = {
idpId: number;
name: string;
variant?: string;
lastUsed?: boolean;
};
type IdpLoginButtonsProps = {
@ -34,6 +35,7 @@ export default function IdpLoginButtons({
orgId
}: IdpLoginButtonsProps) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const t = useTranslations();
const params = useSearchParams();
@ -50,22 +52,10 @@ export default function IdpLoginButtons({
}
}, []);
const [loading, startTransition] = useTransition();
async function loginWithIdp(idpId: number) {
setLoading(true);
setError(null);
setClientCookie(
LAST_USED_IDP_COOKIE_NAME,
JSON.stringify({
orgId,
idpId
}),
{
sameSite: "Lax"
}
);
let redirectToUrl: string | undefined;
try {
console.log("generating", idpId, redirect || "/", orgId);
@ -78,6 +68,7 @@ export default function IdpLoginButtons({
if (response.error) {
setError(response.message);
setLoading(false);
return;
}
@ -93,6 +84,7 @@ export default function IdpLoginButtons({
"An unexpected error occurred. Please try again."
})
);
setLoading(false);
}
if (redirectToUrl) {
@ -132,38 +124,20 @@ export default function IdpLoginButtons({
idp.variant || idp.name.toLowerCase();
return (
<div
className="w-full relative"
<Button
key={idp.idpId}
type="button"
variant="outline"
className="w-full inline-flex items-center space-x-2"
onClick={() => {
loginWithIdp(idp.idpId);
}}
disabled={loading}
loading={loading}
>
<Button
key={idp.idpId}
type="button"
variant="outline"
className="w-full inline-flex items-center space-x-2 after:absolute after:inset-0 after:z-10"
onClick={() => {
startTransition(() =>
loginWithIdp(idp.idpId)
);
}}
disabled={loading}
loading={loading}
>
<IdpTypeIcon
type={effectiveType}
size={16}
/>
<span>{idp.name}</span>
</Button>
{idp.lastUsed && (
<div className="absolute inset-0">
<span className="absolute top-0 right-0 text-xs bg-primary text-primary-foreground rounded-bl-sm rounded-tr-sm px-2 py-0.5">
{t("idpLastUsed")}
</span>
</div>
)}
</div>
<IdpTypeIcon type={effectiveType} size={16} />
<span>{idp.name}</span>
</Button>
);
})}
</>

View file

@ -30,7 +30,10 @@ import Link from "next/link";
import { GenerateOidcUrlResponse } from "@server/routers/idp";
import { Separator } from "./ui/separator";
import { useTranslations } from "next-intl";
import { generateOidcUrlProxy, loginProxy } from "@app/actions/server";
import {
generateOidcUrlProxy,
loginProxy
} from "@app/actions/server";
import { redirect as redirectTo } from "next/navigation";
import { useEnvContext } from "@app/hooks/useEnvContext";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
@ -38,13 +41,11 @@ import IdpTypeIcon from "@app/components/IdpTypeIcon";
import { loadReoScript } from "reodotdev";
import { build } from "@server/build";
import MfaInputForm from "@app/components/MfaInputForm";
import { useLocalStorage } from "@app/hooks/useLocalStorage";
export type LoginFormIDP = {
idpId: number;
name: string;
variant?: string;
lastUsed?: boolean;
};
type LoginFormProps = {
@ -104,6 +105,7 @@ export default function LoginForm({
}
}, []);
const formSchema = z.object({
email: z.string().email({ message: t("emailInvalid") }),
password: z.string().min(8, { message: t("passwordRequirementsChars") })
@ -128,16 +130,11 @@ export default function LoginForm({
}
});
const [lastUsedIdpId, setLastUsedIdpId] = useLocalStorage<string | null>(
"login:last-used-idp",
null
);
async function onSubmit(values: any) {
const { email, password } = form.getValues();
const { code } = mfaForm.getValues();
setLastUsedIdpId(null);
setLoading(true);
setError(null);
@ -182,7 +179,8 @@ export default function LoginForm({
if (data.useSecurityKey) {
setError(
t("securityKeyRequired", {
defaultValue: "Please use your security key to sign in."
defaultValue:
"Please use your security key to sign in."
})
);
return;
@ -244,8 +242,6 @@ export default function LoginForm({
async function loginWithIdp(idpId: number) {
let redirectUrl: string | undefined;
setLastUsedIdpId(idpId.toString());
try {
const data = await generateOidcUrlProxy(
idpId,
@ -360,6 +356,7 @@ export default function LoginForm({
)}
<div className="space-y-4">
{!mfaRequested && (
<>
<SecurityKeyAuthButton
@ -388,41 +385,25 @@ export default function LoginForm({
idp.variant || idp.name.toLowerCase();
return (
<div
className="w-full relative"
<Button
key={idp.idpId}
type="button"
variant="outline"
className="w-full inline-flex items-center space-x-2"
onClick={() => {
loginWithIdp(idp.idpId);
}}
>
<Button
key={idp.idpId}
type="button"
variant="outline"
className="w-full inline-flex items-center space-x-2 after:absolute after:inset-0 after:z-10"
onClick={() => {
loginWithIdp(idp.idpId);
}}
>
<IdpTypeIcon
type={effectiveType}
size={16}
/>
<span>{idp.name}</span>
</Button>
{lastUsedIdpId ===
idp.idpId.toString() && (
<div className="absolute inset-0">
<span className="absolute top-0 right-0 text-xs bg-primary text-primary-foreground rounded-bl-sm rounded-tr-sm px-2 py-0.5">
{t("idpLastUsed")}
</span>
</div>
)}
</div>
<IdpTypeIcon type={effectiveType} size={16} />
<span>{idp.name}</span>
</Button>
);
})}
</>
)}
</>
)}
</div>
</div>
);

View file

@ -22,8 +22,6 @@ import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import MfaInputForm from "@app/components/MfaInputForm";
import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
import { setClientCookie } from "@app/lib/setClientCookie";
type LoginPasswordFormProps = {
identifier: string;
@ -84,12 +82,6 @@ export default function LoginPasswordForm({
const { password } = values;
const { code } = mfaForm.getValues();
// delete last used auth cookie by setting it in the past
setClientCookie(LAST_USED_IDP_COOKIE_NAME, JSON.stringify(null), {
sameSite: "Lax",
days: -1
});
setLoading(true);
setError(null);

View file

@ -1,16 +1,6 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import {
Credenza,
CredenzaBody,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import SiteResourcesOverview from "@app/components/SiteResourcesOverview";
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button";
import {
@ -34,7 +24,6 @@ import {
ArrowUp10Icon,
ArrowUpRight,
Check,
ChevronDown,
ChevronsUpDownIcon,
MoreHorizontal,
X
@ -76,10 +65,8 @@ export default function PendingSitesTable({
const [isRefreshing, startTransition] = useTransition();
const [approvingIds, setApprovingIds] = useState<Set<number>>(new Set());
const [rejectingIds, setRejectingIds] = useState<Set<number>>(new Set());
const [isRejectModalOpen, setIsRejectModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const [resourcesDialogSite, setResourcesDialogSite] =
useState<SiteRow | null>(null);
const api = createApiClient(useEnvContext());
const t = useTranslations();
@ -124,7 +111,7 @@ export default function PendingSitesTable({
async function approveSite(siteId: number) {
setApprovingIds((prev) => new Set(prev).add(siteId));
try {
await api.post(`/site/${siteId}/approve`);
await api.post(`/site/${siteId}`, { status: "approved" });
toast({
title: t("success"),
description: t("siteApproveSuccess"),
@ -149,20 +136,20 @@ export default function PendingSitesTable({
async function rejectSite(siteId: number) {
setRejectingIds((prev) => new Set(prev).add(siteId));
try {
await api.post(`/site/${siteId}/reject`);
await api.delete(`/site/${siteId}`);
toast({
title: t("success"),
description: t("siteRejectSuccess"),
description: t("siteDeleted"),
variant: "default"
});
setIsRejectModalOpen(false);
setIsDeleteModalOpen(false);
setSelectedSite(null);
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("siteRejectError"),
description: formatAxiosError(e, t("siteRejectError"))
title: t("siteErrorDelete"),
description: formatAxiosError(e, t("siteErrorDelete"))
});
} finally {
setRejectingIds((prev) => {
@ -355,29 +342,6 @@ export default function PendingSitesTable({
}
}
},
{
id: "resources",
accessorKey: "resourceCount",
friendlyName: t("resources"),
header: () => <span className="p-3">{t("resources")}</span>,
cell: ({ row }) => {
const siteRow = row.original;
return (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setResourcesDialogSite(siteRow)}
className="flex h-8 items-center gap-2 px-0 font-normal"
>
<span className="text-sm tabular-nums">
{siteRow.resourceCount} {t("resources")}
</span>
<ChevronDown className="h-3 w-3 shrink-0" />
</Button>
);
}
},
{
accessorKey: "exitNode",
friendlyName: t("exitNode"),
@ -481,7 +445,7 @@ export default function PendingSitesTable({
disabled={isApproving || isRejecting}
onClick={() => {
setSelectedSite(siteRow);
setIsRejectModalOpen(true);
setIsDeleteModalOpen(true);
}}
>
<X className="mr-2 w-4 h-4" />
@ -527,63 +491,25 @@ export default function PendingSitesTable({
return (
<>
<Credenza
open={Boolean(resourcesDialogSite)}
onOpenChange={(open) => {
if (!open) setResourcesDialogSite(null);
}}
>
<CredenzaContent className="md:max-w-7xl">
<CredenzaHeader>
<CredenzaTitle>{t("siteResourcesTab")}</CredenzaTitle>
<CredenzaDescription>
{t("siteResourcesDialogDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
{resourcesDialogSite != null && (
<SiteResourcesOverview
orgIdOverride={orgId}
siteId={resourcesDialogSite.id}
initialPublicData={null}
initialPrivateData={null}
initialPublicForbidden={false}
initialPrivateForbidden={false}
showViewAllLinks={false}
/>
)}
</CredenzaBody>
<CredenzaFooter>
<Button
type="button"
variant="outline"
onClick={() => setResourcesDialogSite(null)}
>
{t("close")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
{selectedSite && (
<ConfirmDeleteDialog
open={isRejectModalOpen}
open={isDeleteModalOpen}
setOpen={(val) => {
setIsRejectModalOpen(val);
setIsDeleteModalOpen(val);
if (!val) {
setSelectedSite(null);
}
}}
dialog={
<div className="space-y-2">
<p>{t("siteQuestionReject")}</p>
<p>{t("siteMessageReject")}</p>
<p>{t("siteQuestionRemove")}</p>
<p>{t("siteMessageRemove")}</p>
</div>
}
buttonText={t("siteConfirmReject")}
buttonText={t("siteConfirmDelete")}
onConfirm={async () => rejectSite(selectedSite.id)}
string={selectedSite.name}
title={t("siteReject")}
title={t("siteDelete")}
/>
)}
<ControlledDataTable

View file

@ -55,7 +55,6 @@ function getActionsCategories(root: boolean) {
[t("actionGetSite")]: "getSite",
[t("actionListSites")]: "listSites",
[t("actionUpdateSite")]: "updateSite",
[t("actionUpdateSiteApprovals")]: "updateSiteApprovals",
[t("actionListSiteRoles")]: "listSiteRoles"
},
@ -79,8 +78,7 @@ function getActionsCategories(root: boolean) {
[t("actionGetSiteResource")]: "getSiteResource",
[t("actionListSiteResources")]: "listSiteResources",
[t("actionUpdateSiteResource")]: "updateSiteResource",
[t("actionCreateResourceSessionToken")]:
"createResourceSessionToken"
[t("actionCreateResourceSessionToken")]: "createResourceSessionToken"
},
Target: {

View file

@ -23,7 +23,6 @@ import {
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { Switch } from "@app/components/ui/switch";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
@ -37,9 +36,7 @@ import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHr
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UpdateSiteResourceResponse } from "@server/routers/siteResource";
import type { PaginationState } from "@tanstack/react-table";
import { AxiosResponse } from "axios";
import {
ArrowDown01Icon,
ArrowRight,
@ -52,17 +49,8 @@ import {
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
startTransition,
useMemo,
useOptimistic,
useRef,
useState,
useTransition,
type ComponentRef
} from "react";
import { startTransition, useMemo, useState, useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell";
@ -126,11 +114,6 @@ function isSafeUrlForLink(href: string): boolean {
}
}
const booleanSearchFilterSchema = z
.enum(["true", "false"])
.optional()
.catch(undefined);
type ClientResourcesTableProps = {
internalResources: InternalResourceRow[];
orgId: string;
@ -191,30 +174,6 @@ export default function PrivateResourcesTable({
});
};
async function toggleInternalResourceEnabled(
val: boolean,
resourceId: number
) {
try {
await api.post<AxiosResponse<UpdateSiteResourceResponse>>(
`site-resource/${resourceId}`,
{
enabled: val
}
);
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
}
}
const deleteInternalResource = async (
resourceId: number,
siteId: number
@ -470,36 +429,6 @@ export default function PrivateResourcesTable({
);
}
},
{
accessorKey: "enabled",
friendlyName: t("enabled"),
header: () => (
<ColumnFilterButton
options={[
{ value: "true", label: t("enabled") },
{ value: "false", label: t("disabled") }
]}
selectedValue={booleanSearchFilterSchema.parse(
searchParams.get("enabled")
)}
onValueChange={(value) =>
handleFilterChange("enabled", value)
}
searchPlaceholder={t("searchPlaceholder")}
emptyMessage={t("emptySearchOptions")}
label={t("enabled")}
className="p-3"
/>
),
cell: ({ row }) => (
<InternalResourceEnabledForm
resource={row.original}
onToggleInternalResourceEnabled={
toggleInternalResourceEnabled
}
/>
)
},
{
id: "labels",
accessorKey: "labels",
@ -714,39 +643,3 @@ function ClientResourceLabelCell({
/>
);
}
type InternalResourceEnabledFormProps = {
resource: InternalResourceRow;
onToggleInternalResourceEnabled: (
val: boolean,
resourceId: number
) => Promise<void>;
};
function InternalResourceEnabledForm({
resource,
onToggleInternalResourceEnabled
}: InternalResourceEnabledFormProps) {
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(
resource.enabled
);
const formRef = useRef<ComponentRef<"form">>(null);
async function submitAction(formData: FormData) {
const newEnabled = !(formData.get("enabled") === "on");
setOptimisticEnabled(newEnabled);
await onToggleInternalResourceEnabled(newEnabled, resource.id);
}
return (
<form action={submitAction} ref={formRef}>
<Switch
checked={optimisticEnabled}
disabled={optimisticEnabled !== resource.enabled}
name="enabled"
onCheckedChange={() => formRef.current?.requestSubmit()}
/>
</form>
);
}

View file

@ -174,8 +174,8 @@ type OverviewRow = {
type OverviewColumnProps = {
title: string;
description: string;
viewAllHref?: string;
viewAllLabel?: string;
viewAllHref: string;
viewAllLabel: string;
emptyLabel: string;
isForbidden: boolean;
isFetching: boolean;
@ -212,14 +212,12 @@ function OverviewColumn({
{description}
</p>
</div>
{viewAllHref && viewAllLabel ? (
<Link
href={viewAllHref}
className="shrink-0 text-muted-foreground text-sm hover:underline"
>
{viewAllLabel}
</Link>
) : null}
<Link
href={viewAllHref}
className="shrink-0 text-muted-foreground text-sm hover:underline"
>
{viewAllLabel}
</Link>
</div>
</div>
);
@ -321,8 +319,6 @@ type SiteResourcesOverviewProps = {
initialPrivateForbidden: boolean;
/** When not under `/[orgId]/...` routes, pass org id explicitly (e.g. credenza on sites list). */
orgIdOverride?: string;
/** When false, hides links to the org resources tables filtered by this site. */
showViewAllLinks?: boolean;
};
export default function SiteResourcesOverview({
@ -331,8 +327,7 @@ export default function SiteResourcesOverview({
initialPrivateData,
initialPublicForbidden,
initialPrivateForbidden,
orgIdOverride,
showViewAllLinks = true
orgIdOverride
}: SiteResourcesOverviewProps) {
const t = useTranslations();
const params = useParams<{ orgId: string }>();
@ -472,10 +467,8 @@ export default function SiteResourcesOverview({
key="public"
title={t("siteResourcesSectionPublic")}
description={t("siteResourcesSectionPublicDescription")}
viewAllHref={showViewAllLinks ? publicViewAllHref : undefined}
viewAllLabel={
showViewAllLinks ? t("siteResourcesViewAllPublic") : undefined
}
viewAllHref={publicViewAllHref}
viewAllLabel={t("siteResourcesViewAllPublic")}
emptyLabel={t("siteResourcesEmptyPublic")}
isForbidden={publicForbidden}
isFetching={publicQuery.isFetching}
@ -491,10 +484,8 @@ export default function SiteResourcesOverview({
key="private"
title={t("siteResourcesSectionPrivate")}
description={t("siteResourcesSectionPrivateDescription")}
viewAllHref={showViewAllLinks ? privateViewAllHref : undefined}
viewAllLabel={
showViewAllLinks ? t("siteResourcesViewAllPrivate") : undefined
}
viewAllHref={privateViewAllHref}
viewAllLabel={t("siteResourcesViewAllPrivate")}
emptyLabel={t("siteResourcesEmptyPrivate")}
isForbidden={privateForbidden}
isFetching={privateQuery.isFetching}

View file

@ -27,8 +27,6 @@ import UserProfileCard from "@app/components/UserProfileCard";
import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton";
import { Separator } from "@app/components/ui/separator";
import OrgSignInLink from "@app/components/OrgSignInLink";
import type { LoginFormIDP } from "./LoginForm";
import IdpLoginButtons from "./IdpLoginButtons";
const identifierSchema = z.object({
identifier: z.string().min(1, "Username or email is required")
@ -55,7 +53,6 @@ type SmartLoginFormProps = {
forceLogin?: boolean;
defaultUser?: string;
orgSignIn?: OrgSignInConfig;
lastUsedIdp?: (LoginFormIDP & { orgId?: string }) | null;
};
type ViewState =
@ -92,8 +89,7 @@ export default function SmartLoginForm({
redirect,
forceLogin,
defaultUser,
orgSignIn,
lastUsedIdp
orgSignIn
}: SmartLoginFormProps) {
const router = useRouter();
const { env } = useEnvContext();
@ -298,15 +294,6 @@ export default function SmartLoginForm({
</span>
</div>
</div>
{lastUsedIdp && (
<IdpLoginButtons
idps={[lastUsedIdp]}
orgId={lastUsedIdp.orgId}
redirect={redirect}
/>
)}
<OrgSignInLink
href={orgSignIn.href}
linkText={orgSignIn.linkText}

View file

@ -17,8 +17,6 @@ import {
} from "next/navigation";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Separator } from "@app/components/ui/separator";
import { setClientCookie } from "@app/lib/setClientCookie";
import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
type SmartLoginOrgSelectorProps = {
identifier: string;
@ -143,17 +141,6 @@ export default function SmartLoginOrgSelector({
setPendingIdpId(idpId);
setError(null);
setClientCookie(
LAST_USED_IDP_COOKIE_NAME,
JSON.stringify({
orgId,
idpId
}),
{
sameSite: "Lax"
}
);
let redirectToUrl: string | undefined;
try {
const safeRedirect = cleanRedirect(redirect || "/");

View file

@ -4,13 +4,9 @@ import { getCachedSubscription } from "./getCachedSubscription";
import { priv } from ".";
import { AxiosResponse } from "axios";
import { GetLicenseStatusResponse } from "@server/routers/license/types";
import { Tier } from "@server/types/Tiers";
const DEFAULT_PAID_TIERS: Tier[] = ["tier1", "tier2", "tier3", "enterprise"];
export const isOrgSubscribed = cache(async (orgId: string, tiers?: Tier[]) => {
export const isOrgSubscribed = cache(async (orgId: string) => {
let subscribed = false;
const allowedTiers = tiers ?? DEFAULT_PAID_TIERS;
if (build === "enterprise") {
try {
@ -24,8 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string, tiers?: Tier[]) => {
try {
const subRes = await getCachedSubscription(orgId);
subscribed =
!!subRes.data.data.tier &&
allowedTiers.includes(subRes.data.data.tier as Tier) &&
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3" || subRes.data.data.tier == "enterprise") &&
subRes.data.data.active;
} catch {}
}

View file

@ -1 +0,0 @@
export const LAST_USED_IDP_COOKIE_NAME = "p__last_used_idp";

View file

@ -1,7 +1,6 @@
import { priv } from "@app/lib/api";
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types";
import { AxiosResponse } from "axios";
@ -12,10 +11,7 @@ export async function loadOrgLoginPageBranding(orgId: string): Promise<{
return { primaryColor: null };
}
const subscribed = await isOrgSubscribed(
orgId,
tierMatrix.loginPageBranding
);
const subscribed = await isOrgSubscribed(orgId);
if (!subscribed) {
return { primaryColor: null };
}

View file

@ -165,6 +165,7 @@ export function buildCreateSiteResourcePayload(
siteIds: data.siteIds,
mode: data.mode,
destination: isNativeSsh ? undefined : (data.destination ?? undefined),
enabled: true,
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
@ -341,8 +342,7 @@ export function createGeneralFormSchema(t: TranslateFn) {
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/),
enabled: z.boolean()
.regex(/^[a-zA-Z0-9-]+$/)
});
}

View file

@ -1,32 +0,0 @@
/**
* Set a cookie on the client side in javascript code, not on the server
* @param name
* @param value
* @param days
* @param options
*/
export function setClientCookie(
name: string,
value: string,
options: {
days?: number;
path?: string;
secure?: boolean;
sameSite?: "Strict" | "Lax" | "None";
} = {}
): void {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.days) {
const date = new Date();
date.setTime(date.getTime() + options.days * 864e5);
cookie += `; expires=${date.toUTCString()}`;
}
cookie += `; path=${options.path ?? "/"}`;
if (options.secure) cookie += "; Secure";
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
document.cookie = cookie;
}