mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-18 11:36:30 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c30fe5b574 | |||
| a0b9ea76a3 | |||
| eb3a3eac98 | |||
| 02d2e09709 | |||
| 6e8283957c | |||
| bb9b94a983 | |||
| 36f807ddbc | |||
| cb31546c6b | |||
| 2d48adc9f9 | |||
| 113b7a0b84 | |||
| ca279ee3e0 | |||
| a5808200f0 | |||
| aa9de4d2ac | |||
| 50821e972d | |||
| e5b9dfee66 | |||
| 8e09070bc1 | |||
| 3d9c413bbb | |||
| 86670c1d60 | |||
| 5f2ee77a73 | |||
| 714e3a8fb1 | |||
| 672555f051 | |||
| 7853f2b096 | |||
| 61a7dda29a | |||
| f9bcb25eaa | |||
| 6df804ab32 | |||
| 9e50735800 | |||
| a30119f5e8 | |||
| 3c9f0946d2 | |||
| 21861a6dcd | |||
| 6135f2b727 | |||
| 3a616cc804 | |||
| d5eb67a8fc | |||
| 3ac7ef23ae | |||
| 08c5ec2be7 | |||
| a88b79e066 | |||
| a48ef77ee5 | |||
| 55f3807491 | |||
| dbfb8e83e8 | |||
| 515afc14a5 | |||
| 903e8c0fa1 | |||
| 7f9c760380 | |||
| 530a1a5350 | |||
| 3fb36c1434 |
+1
-1
@@ -76,7 +76,7 @@ var redisFlag *bool
|
|||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
|
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
|
||||||
redisFlag = flag.Bool("redis", false, "Install Redis as cacheing solution. Required for HA. Not required for the Enterprise version.")
|
redisFlag = flag.Bool("redis", false, "Install Redis as caching solution. Required for HA. Not required for the Enterprise version.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Списък с покани",
|
"actionListInvitations": "Списък с покани",
|
||||||
"actionExportLogs": "Експортиране на дневници",
|
"actionExportLogs": "Експортиране на дневници",
|
||||||
"actionViewLogs": "Преглед на дневници",
|
"actionViewLogs": "Преглед на дневници",
|
||||||
|
"actionCreateSiteProvisioningKey": "Създаване на ключ за предоставяне на сайт",
|
||||||
|
"actionListSiteProvisioningKeys": "Списък на ключовете за предоставяне на сайт",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Актуализиране на ключ за предоставяне на сайт",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Изтриване на ключ за предоставяне на сайт",
|
||||||
"noneSelected": "Нищо не е избрано",
|
"noneSelected": "Нищо не е избрано",
|
||||||
"orgNotFound2": "Няма намерени организации.",
|
"orgNotFound2": "Няма намерени организации.",
|
||||||
"search": "Търси…",
|
"search": "Търси…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Качването неуспешно",
|
"rdpUploadFailed": "Качването неуспешно",
|
||||||
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
|
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
|
||||||
"sessionToolbarShow": "Показване на лентата с инструменти",
|
"sessionToolbarShow": "Показване на лентата с инструменти",
|
||||||
"sessionToolbarHide": "Скриване на лентата с инструменти"
|
"sessionToolbarHide": "Скриване на лентата с инструменти",
|
||||||
|
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Seznam pozvánek",
|
"actionListInvitations": "Seznam pozvánek",
|
||||||
"actionExportLogs": "Exportovat protokoly",
|
"actionExportLogs": "Exportovat protokoly",
|
||||||
"actionViewLogs": "Zobrazit logy",
|
"actionViewLogs": "Zobrazit logy",
|
||||||
|
"actionCreateSiteProvisioningKey": "Vytvořit klíč pro zřízení webu",
|
||||||
|
"actionListSiteProvisioningKeys": "Seznam klíčů pro zřízení webu",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Aktualizovat klíč pro zřízení webu",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Smazat klíč pro zřízení webu",
|
||||||
"noneSelected": "Není vybráno",
|
"noneSelected": "Není vybráno",
|
||||||
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
|
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
|
||||||
"search": "Vyhledávání…",
|
"search": "Vyhledávání…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Nahrání selhalo",
|
"rdpUploadFailed": "Nahrání selhalo",
|
||||||
"rdpUnicodeKeyboardMode": "Režim Unicode klávesnice",
|
"rdpUnicodeKeyboardMode": "Režim Unicode klávesnice",
|
||||||
"sessionToolbarShow": "Zobrazit panel nástrojů",
|
"sessionToolbarShow": "Zobrazit panel nástrojů",
|
||||||
"sessionToolbarHide": "Skrýt panel nástrojů"
|
"sessionToolbarHide": "Skrýt panel nástrojů",
|
||||||
|
"actionUpdateSiteApprovals": "Aktualizovat schválení webu"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Vis invitationer",
|
"actionListInvitations": "Vis invitationer",
|
||||||
"actionExportLogs": "Eksportér logs",
|
"actionExportLogs": "Eksportér logs",
|
||||||
"actionViewLogs": "Vis logs",
|
"actionViewLogs": "Vis logs",
|
||||||
|
"actionCreateSiteProvisioningKey": "Opret provisioneringsnøgle til site",
|
||||||
|
"actionListSiteProvisioningKeys": "Vis provisioneringsnøgler til site",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Opdater provisioneringsnøgle til site",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Slet provisioneringsnøgle til site",
|
||||||
"noneSelected": "Ingen valgt",
|
"noneSelected": "Ingen valgt",
|
||||||
"orgNotFound2": "Ingen organisationer fundet.",
|
"orgNotFound2": "Ingen organisationer fundet.",
|
||||||
"search": "Søg…",
|
"search": "Søg…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Uploaden mislykkedes",
|
"rdpUploadFailed": "Uploaden mislykkedes",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
|
"rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
|
||||||
"sessionToolbarShow": "Vis værktøjslinje",
|
"sessionToolbarShow": "Vis værktøjslinje",
|
||||||
"sessionToolbarHide": "Skjul værktøjslinje"
|
"sessionToolbarHide": "Skjul værktøjslinje",
|
||||||
|
"actionUpdateSiteApprovals": "Opdater godkendelser på site"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Einladungen auflisten",
|
"actionListInvitations": "Einladungen auflisten",
|
||||||
"actionExportLogs": "Logs exportieren",
|
"actionExportLogs": "Logs exportieren",
|
||||||
"actionViewLogs": "Logs anzeigen",
|
"actionViewLogs": "Logs anzeigen",
|
||||||
|
"actionCreateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort erstellen",
|
||||||
|
"actionListSiteProvisioningKeys": "Liste der Bereitstellungsschlüssel für Standorte",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort aktualisieren",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Bereitstellungsschlüssel für Standort löschen",
|
||||||
"noneSelected": "Keine ausgewählt",
|
"noneSelected": "Keine ausgewählt",
|
||||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||||
"search": "Suche…",
|
"search": "Suche…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Upload fehlgeschlagen",
|
"rdpUploadFailed": "Upload fehlgeschlagen",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus",
|
"rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus",
|
||||||
"sessionToolbarShow": "Werkzeugleiste zeigen",
|
"sessionToolbarShow": "Werkzeugleiste zeigen",
|
||||||
"sessionToolbarHide": "Werkzeugleiste ausblenden"
|
"sessionToolbarHide": "Werkzeugleiste ausblenden",
|
||||||
|
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -178,7 +178,7 @@
|
|||||||
"shareDeleteConfirm": "Confirm Delete Shareable Link",
|
"shareDeleteConfirm": "Confirm Delete Shareable Link",
|
||||||
"shareQuestionRemove": "Are you sure you want to delete this share link?",
|
"shareQuestionRemove": "Are you sure you want to delete this share link?",
|
||||||
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
|
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
|
||||||
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
"shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"usageExamples": "Usage Examples",
|
"usageExamples": "Usage Examples",
|
||||||
"tokenId": "Token ID",
|
"tokenId": "Token ID",
|
||||||
@@ -196,8 +196,14 @@
|
|||||||
"shareTitleOptional": "Title (optional)",
|
"shareTitleOptional": "Title (optional)",
|
||||||
"sharePathOptional": "Path (optional)",
|
"sharePathOptional": "Path (optional)",
|
||||||
"sharePathDescription": "The link will redirect users to this path after authentication.",
|
"sharePathDescription": "The link will redirect users to this path after authentication.",
|
||||||
|
"shareAssociateUserOptional": "Associate User (optional)",
|
||||||
|
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
|
||||||
|
"userSelect": "Select user",
|
||||||
|
"usersNotFound": "No users found",
|
||||||
"expireIn": "Expire In",
|
"expireIn": "Expire In",
|
||||||
"neverExpire": "Never expire",
|
"neverExpire": "Never expire",
|
||||||
|
"sharePersistSession": "Persist session after first use",
|
||||||
|
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
|
||||||
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
|
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
|
||||||
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
|
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
|
||||||
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
||||||
@@ -1511,6 +1517,10 @@
|
|||||||
"actionListInvitations": "List Invitations",
|
"actionListInvitations": "List Invitations",
|
||||||
"actionExportLogs": "Export Logs",
|
"actionExportLogs": "Export Logs",
|
||||||
"actionViewLogs": "View Logs",
|
"actionViewLogs": "View Logs",
|
||||||
|
"actionCreateSiteProvisioningKey": "Create Site Provisioning Key",
|
||||||
|
"actionListSiteProvisioningKeys": "List Site Provisioning Keys",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Update Site Provisioning Key",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Delete Site Provisioning Key",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"search": "Search…",
|
"search": "Search…",
|
||||||
@@ -3085,8 +3095,8 @@
|
|||||||
"sourceAddress": "Source Address",
|
"sourceAddress": "Source Address",
|
||||||
"destinationAddress": "Destination Address",
|
"destinationAddress": "Destination Address",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
||||||
"certResolver": "Certificate Resolver",
|
"certResolver": "Certificate Resolver",
|
||||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||||
"selectCertResolver": "Select Certificate Resolver",
|
"selectCertResolver": "Select Certificate Resolver",
|
||||||
@@ -3811,5 +3821,6 @@
|
|||||||
"rdpUploadFailed": "Upload failed",
|
"rdpUploadFailed": "Upload failed",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
|
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
|
||||||
"sessionToolbarShow": "Show toolbar",
|
"sessionToolbarShow": "Show toolbar",
|
||||||
"sessionToolbarHide": "Hide toolbar"
|
"sessionToolbarHide": "Hide toolbar",
|
||||||
|
"actionUpdateSiteApprovals": "Update Site Approvals"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Listar invitaciones",
|
"actionListInvitations": "Listar invitaciones",
|
||||||
"actionExportLogs": "Exportar registros",
|
"actionExportLogs": "Exportar registros",
|
||||||
"actionViewLogs": "Ver registros",
|
"actionViewLogs": "Ver registros",
|
||||||
|
"actionCreateSiteProvisioningKey": "Crear clave de aprovisionamiento del sitio",
|
||||||
|
"actionListSiteProvisioningKeys": "Listado de claves de aprovisionamiento del sitio",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Actualizar clave de aprovisionamiento del sitio",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Eliminar clave de aprovisionamiento del sitio",
|
||||||
"noneSelected": "Ninguno seleccionado",
|
"noneSelected": "Ninguno seleccionado",
|
||||||
"orgNotFound2": "No se encontraron organizaciones.",
|
"orgNotFound2": "No se encontraron organizaciones.",
|
||||||
"search": "Buscar…",
|
"search": "Buscar…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Error de subida",
|
"rdpUploadFailed": "Error de subida",
|
||||||
"rdpUnicodeKeyboardMode": "Modo teclado Unicode",
|
"rdpUnicodeKeyboardMode": "Modo teclado Unicode",
|
||||||
"sessionToolbarShow": "Mostrar barra de herramientas",
|
"sessionToolbarShow": "Mostrar barra de herramientas",
|
||||||
"sessionToolbarHide": "Ocultar barra de herramientas"
|
"sessionToolbarHide": "Ocultar barra de herramientas",
|
||||||
|
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Lister les invitations",
|
"actionListInvitations": "Lister les invitations",
|
||||||
"actionExportLogs": "Exporter les journaux",
|
"actionExportLogs": "Exporter les journaux",
|
||||||
"actionViewLogs": "Voir les logs",
|
"actionViewLogs": "Voir les logs",
|
||||||
|
"actionCreateSiteProvisioningKey": "Créer une clé de provisionnement de site",
|
||||||
|
"actionListSiteProvisioningKeys": "Lister les clés de provisionnement de site",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Mettre à jour la clé de provisionnement de site",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Supprimer la clé de provisionnement de site",
|
||||||
"noneSelected": "Aucune sélection",
|
"noneSelected": "Aucune sélection",
|
||||||
"orgNotFound2": "Aucune organisation trouvée.",
|
"orgNotFound2": "Aucune organisation trouvée.",
|
||||||
"search": "Rechercher…",
|
"search": "Rechercher…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Échec du téléchargement",
|
"rdpUploadFailed": "Échec du téléchargement",
|
||||||
"rdpUnicodeKeyboardMode": "Mode clavier Unicode",
|
"rdpUnicodeKeyboardMode": "Mode clavier Unicode",
|
||||||
"sessionToolbarShow": "Afficher la barre d'outils",
|
"sessionToolbarShow": "Afficher la barre d'outils",
|
||||||
"sessionToolbarHide": "Masquer la barre d'outils"
|
"sessionToolbarHide": "Masquer la barre d'outils",
|
||||||
|
"actionUpdateSiteApprovals": "Mettre à jour les approbations de site"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Elenco Inviti",
|
"actionListInvitations": "Elenco Inviti",
|
||||||
"actionExportLogs": "Esporta Log",
|
"actionExportLogs": "Esporta Log",
|
||||||
"actionViewLogs": "Visualizza Log",
|
"actionViewLogs": "Visualizza Log",
|
||||||
|
"actionCreateSiteProvisioningKey": "Crea Chiave di Provisioning del Sito",
|
||||||
|
"actionListSiteProvisioningKeys": "Elenca Chiavi di Provisioning del Sito",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Aggiorna Chiave di Provisioning del Sito",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Elimina Chiave di Provisioning del Sito",
|
||||||
"noneSelected": "Nessuna selezione",
|
"noneSelected": "Nessuna selezione",
|
||||||
"orgNotFound2": "Nessuna organizzazione trovata.",
|
"orgNotFound2": "Nessuna organizzazione trovata.",
|
||||||
"search": "Cerca…",
|
"search": "Cerca…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Caricamento fallito",
|
"rdpUploadFailed": "Caricamento fallito",
|
||||||
"rdpUnicodeKeyboardMode": "Modalità tastiera Unicode",
|
"rdpUnicodeKeyboardMode": "Modalità tastiera Unicode",
|
||||||
"sessionToolbarShow": "Mostra barra degli strumenti",
|
"sessionToolbarShow": "Mostra barra degli strumenti",
|
||||||
"sessionToolbarHide": "Nascondi barra degli strumenti"
|
"sessionToolbarHide": "Nascondi barra degli strumenti",
|
||||||
|
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "초대 목록",
|
"actionListInvitations": "초대 목록",
|
||||||
"actionExportLogs": "로그 내보내기",
|
"actionExportLogs": "로그 내보내기",
|
||||||
"actionViewLogs": "로그 보기",
|
"actionViewLogs": "로그 보기",
|
||||||
|
"actionCreateSiteProvisioningKey": "사이트 프로비저닝 키 생성",
|
||||||
|
"actionListSiteProvisioningKeys": "사이트 프로비저닝 키 목록",
|
||||||
|
"actionUpdateSiteProvisioningKey": "사이트 프로비저닝 키 업데이트",
|
||||||
|
"actionDeleteSiteProvisioningKey": "사이트 프로비저닝 키 삭제",
|
||||||
"noneSelected": "선택된 항목 없음",
|
"noneSelected": "선택된 항목 없음",
|
||||||
"orgNotFound2": "조직이 없습니다.",
|
"orgNotFound2": "조직이 없습니다.",
|
||||||
"search": "검색…",
|
"search": "검색…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "업로드 실패",
|
"rdpUploadFailed": "업로드 실패",
|
||||||
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
|
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
|
||||||
"sessionToolbarShow": "툴바 보기",
|
"sessionToolbarShow": "툴바 보기",
|
||||||
"sessionToolbarHide": "툴바 숨기기"
|
"sessionToolbarHide": "툴바 숨기기",
|
||||||
|
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Liste invitasjoner",
|
"actionListInvitations": "Liste invitasjoner",
|
||||||
"actionExportLogs": "Eksportlogger",
|
"actionExportLogs": "Eksportlogger",
|
||||||
"actionViewLogs": "Vis logger",
|
"actionViewLogs": "Vis logger",
|
||||||
|
"actionCreateSiteProvisioningKey": "Opprett Klargjøringsnøkkel for Sted",
|
||||||
|
"actionListSiteProvisioningKeys": "List Klargjøringsnøkler for Sted",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Oppdater Klargjøringsnøkkel for Sted",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Slett Klargjøringsnøkkel for Sted",
|
||||||
"noneSelected": "Ingen valgt",
|
"noneSelected": "Ingen valgt",
|
||||||
"orgNotFound2": "Ingen organisasjoner funnet.",
|
"orgNotFound2": "Ingen organisasjoner funnet.",
|
||||||
"search": "Søk…",
|
"search": "Søk…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Opplastningen mislyktes",
|
"rdpUploadFailed": "Opplastningen mislyktes",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
|
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
|
||||||
"sessionToolbarShow": "Vis verktøylinje",
|
"sessionToolbarShow": "Vis verktøylinje",
|
||||||
"sessionToolbarHide": "Skjul verktøylinje"
|
"sessionToolbarHide": "Skjul verktøylinje",
|
||||||
|
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Toon uitnodigingen",
|
"actionListInvitations": "Toon uitnodigingen",
|
||||||
"actionExportLogs": "Logboeken exporteren",
|
"actionExportLogs": "Logboeken exporteren",
|
||||||
"actionViewLogs": "Logboeken bekijken",
|
"actionViewLogs": "Logboeken bekijken",
|
||||||
|
"actionCreateSiteProvisioningKey": "Een sitevoorzie-ningssleutel aanmaken",
|
||||||
|
"actionListSiteProvisioningKeys": "Sitevoorzie-ningssleutels weergeven",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Sitevoorzie-ningssleutel bijwerken",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Sitevoorzie-ningssleutel verwijderen",
|
||||||
"noneSelected": "Niet geselecteerd",
|
"noneSelected": "Niet geselecteerd",
|
||||||
"orgNotFound2": "Geen organisaties gevonden.",
|
"orgNotFound2": "Geen organisaties gevonden.",
|
||||||
"search": "Zoeken…",
|
"search": "Zoeken…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Upload mislukt",
|
"rdpUploadFailed": "Upload mislukt",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus",
|
"rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus",
|
||||||
"sessionToolbarShow": "Toon werkbalk",
|
"sessionToolbarShow": "Toon werkbalk",
|
||||||
"sessionToolbarHide": "Verberg werkbalk"
|
"sessionToolbarHide": "Verberg werkbalk",
|
||||||
|
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Lista zaproszeń",
|
"actionListInvitations": "Lista zaproszeń",
|
||||||
"actionExportLogs": "Eksportuj dzienniki",
|
"actionExportLogs": "Eksportuj dzienniki",
|
||||||
"actionViewLogs": "Zobacz dzienniki",
|
"actionViewLogs": "Zobacz dzienniki",
|
||||||
|
"actionCreateSiteProvisioningKey": "Utwórz klucz konfiguracji strony",
|
||||||
|
"actionListSiteProvisioningKeys": "Lista kluczy konfiguracji strony",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Zaktualizuj klucz konfiguracji strony",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Usuń klucz konfiguracji strony",
|
||||||
"noneSelected": "Nie wybrano",
|
"noneSelected": "Nie wybrano",
|
||||||
"orgNotFound2": "Nie znaleziono organizacji.",
|
"orgNotFound2": "Nie znaleziono organizacji.",
|
||||||
"search": "Szukaj…",
|
"search": "Szukaj…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Niepowodzenie przesyłania",
|
"rdpUploadFailed": "Niepowodzenie przesyłania",
|
||||||
"rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode",
|
"rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode",
|
||||||
"sessionToolbarShow": "Pokaż pasek narzędzi",
|
"sessionToolbarShow": "Pokaż pasek narzędzi",
|
||||||
"sessionToolbarHide": "Ukryj pasek narzędzi"
|
"sessionToolbarHide": "Ukryj pasek narzędzi",
|
||||||
|
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Listar Convites",
|
"actionListInvitations": "Listar Convites",
|
||||||
"actionExportLogs": "Exportar logs",
|
"actionExportLogs": "Exportar logs",
|
||||||
"actionViewLogs": "Visualizar registros",
|
"actionViewLogs": "Visualizar registros",
|
||||||
|
"actionCreateSiteProvisioningKey": "Criar Chave de Provisionamento do Site",
|
||||||
|
"actionListSiteProvisioningKeys": "Listar Chaves de Provisionamento do Site",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Atualizar Chave de Provisionamento do Site",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Excluir Chave de Provisionamento do Site",
|
||||||
"noneSelected": "Nenhum selecionado",
|
"noneSelected": "Nenhum selecionado",
|
||||||
"orgNotFound2": "Nenhuma organização encontrada.",
|
"orgNotFound2": "Nenhuma organização encontrada.",
|
||||||
"search": "Pesquisar…",
|
"search": "Pesquisar…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Falha no upload",
|
"rdpUploadFailed": "Falha no upload",
|
||||||
"rdpUnicodeKeyboardMode": "Modo de teclado Unicode",
|
"rdpUnicodeKeyboardMode": "Modo de teclado Unicode",
|
||||||
"sessionToolbarShow": "Mostrar barra de ferramentas",
|
"sessionToolbarShow": "Mostrar barra de ferramentas",
|
||||||
"sessionToolbarHide": "Ocultar barra de ferramentas"
|
"sessionToolbarHide": "Ocultar barra de ferramentas",
|
||||||
|
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Список приглашений",
|
"actionListInvitations": "Список приглашений",
|
||||||
"actionExportLogs": "Экспорт журналов",
|
"actionExportLogs": "Экспорт журналов",
|
||||||
"actionViewLogs": "Просмотр журналов",
|
"actionViewLogs": "Просмотр журналов",
|
||||||
|
"actionCreateSiteProvisioningKey": "Создать ключ конфигурации сайта",
|
||||||
|
"actionListSiteProvisioningKeys": "Список ключей конфигурации сайтов",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Обновить ключ конфигурации сайта",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Удалить ключ конфигурации сайта",
|
||||||
"noneSelected": "Ничего не выбрано",
|
"noneSelected": "Ничего не выбрано",
|
||||||
"orgNotFound2": "Организации не найдены.",
|
"orgNotFound2": "Организации не найдены.",
|
||||||
"search": "Поиск…",
|
"search": "Поиск…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Ошибка загрузки",
|
"rdpUploadFailed": "Ошибка загрузки",
|
||||||
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
|
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
|
||||||
"sessionToolbarShow": "Показать панель инструментов",
|
"sessionToolbarShow": "Показать панель инструментов",
|
||||||
"sessionToolbarHide": "Скрыть панель инструментов"
|
"sessionToolbarHide": "Скрыть панель инструментов",
|
||||||
|
"actionUpdateSiteApprovals": "Обновить утверждения сайта"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "Davetiyeleri Listele",
|
"actionListInvitations": "Davetiyeleri Listele",
|
||||||
"actionExportLogs": "Kayıtları Dışa Aktar",
|
"actionExportLogs": "Kayıtları Dışa Aktar",
|
||||||
"actionViewLogs": "Kayıtları Görüntüle",
|
"actionViewLogs": "Kayıtları Görüntüle",
|
||||||
|
"actionCreateSiteProvisioningKey": "Site Sağlama Anahtarı Oluştur",
|
||||||
|
"actionListSiteProvisioningKeys": "Site Sağlama Anahtarlarını Listele",
|
||||||
|
"actionUpdateSiteProvisioningKey": "Site Sağlama Anahtarını Güncelle",
|
||||||
|
"actionDeleteSiteProvisioningKey": "Site Sağlama Anahtarını Sil",
|
||||||
"noneSelected": "Hiçbiri seçili değil",
|
"noneSelected": "Hiçbiri seçili değil",
|
||||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||||
"search": "Ara…",
|
"search": "Ara…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "Yükleme başarısız",
|
"rdpUploadFailed": "Yükleme başarısız",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode klavye modu",
|
"rdpUnicodeKeyboardMode": "Unicode klavye modu",
|
||||||
"sessionToolbarShow": "Araç çubuğunu göster",
|
"sessionToolbarShow": "Araç çubuğunu göster",
|
||||||
"sessionToolbarHide": "Araç çubuğunu gizle"
|
"sessionToolbarHide": "Araç çubuğunu gizle",
|
||||||
|
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1511,6 +1511,10 @@
|
|||||||
"actionListInvitations": "邀请列表",
|
"actionListInvitations": "邀请列表",
|
||||||
"actionExportLogs": "导出日志",
|
"actionExportLogs": "导出日志",
|
||||||
"actionViewLogs": "查看日志",
|
"actionViewLogs": "查看日志",
|
||||||
|
"actionCreateSiteProvisioningKey": "创建站点预配密钥",
|
||||||
|
"actionListSiteProvisioningKeys": "列出站点预配密钥",
|
||||||
|
"actionUpdateSiteProvisioningKey": "更新站点预配密钥",
|
||||||
|
"actionDeleteSiteProvisioningKey": "删除站点预配密钥",
|
||||||
"noneSelected": "未选择",
|
"noneSelected": "未选择",
|
||||||
"orgNotFound2": "未找到组织。",
|
"orgNotFound2": "未找到组织。",
|
||||||
"search": "搜索…",
|
"search": "搜索…",
|
||||||
@@ -3811,5 +3815,6 @@
|
|||||||
"rdpUploadFailed": "上传失败",
|
"rdpUploadFailed": "上传失败",
|
||||||
"rdpUnicodeKeyboardMode": "Unicode 键盘模式",
|
"rdpUnicodeKeyboardMode": "Unicode 键盘模式",
|
||||||
"sessionToolbarShow": "显示工具栏",
|
"sessionToolbarShow": "显示工具栏",
|
||||||
"sessionToolbarHide": "隐藏工具栏"
|
"sessionToolbarHide": "隐藏工具栏",
|
||||||
|
"actionUpdateSiteApprovals": "更新站点审批"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const sites = pgTable(
|
|||||||
lastPing: integer("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
address: varchar("address"),
|
address: varchar("address"),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
|
localEndpoints: varchar("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
|
||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
@@ -905,12 +906,16 @@ export const resourceAccessToken = pgTable("resourceAccessToken", {
|
|||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
path: varchar("path"),
|
path: varchar("path"),
|
||||||
tokenHash: varchar("tokenHash").notNull(),
|
tokenHash: varchar("tokenHash").notNull(),
|
||||||
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
|
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||||
title: varchar("title"),
|
title: varchar("title"),
|
||||||
description: varchar("description"),
|
description: varchar("description"),
|
||||||
|
persistSession: boolean("persistSession").notNull().default(false),
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
// exit node stuff that is how to connect to the site when it has a wg server
|
// exit node stuff that is how to connect to the site when it has a wg server
|
||||||
address: text("address"), // this is the address of the wireguard interface in newt
|
address: text("address"), // this is the address of the wireguard interface in newt
|
||||||
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
|
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
|
||||||
|
localEndpoints: text("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
|
||||||
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
|
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
@@ -1125,12 +1126,18 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
|||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
tokenHash: text("tokenHash").notNull(),
|
tokenHash: text("tokenHash").notNull(),
|
||||||
sessionLength: integer("sessionLength").notNull(),
|
sessionLength: integer("sessionLength").notNull(),
|
||||||
expiresAt: integer("expiresAt"),
|
expiresAt: integer("expiresAt"),
|
||||||
title: text("title"),
|
title: text("title"),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
|
persistSession: integer("persistSession", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
createdAt: integer("createdAt").notNull()
|
createdAt: integer("createdAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,31 @@ export async function updatePrivateResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourceData.alias) {
|
||||||
|
const [aliasConflict] = await trx
|
||||||
|
.select({
|
||||||
|
siteResourceId: siteResources.siteResourceId
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.alias, resourceData.alias),
|
||||||
|
ne(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
existingResource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (aliasConflict) {
|
||||||
|
throw new Error(
|
||||||
|
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
const [updatedResource] = await trx
|
const [updatedResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
@@ -480,6 +505,27 @@ export async function updatePrivateResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourceData.alias) {
|
||||||
|
const [aliasConflict] = await trx
|
||||||
|
.select({
|
||||||
|
siteResourceId: siteResources.siteResourceId
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.alias, resourceData.alias)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (aliasConflict) {
|
||||||
|
throw new Error(
|
||||||
|
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [network] = await trx
|
const [network] = await trx
|
||||||
.insert(networks)
|
.insert(networks)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.20.0";
|
export const APP_VERSION = "1.21.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
+49
-18
@@ -8,26 +8,42 @@ const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
|||||||
function statusHistoryCacheKey(
|
function statusHistoryCacheKey(
|
||||||
entityType: string,
|
entityType: string,
|
||||||
entityId: number,
|
entityId: number,
|
||||||
days: number
|
days: number,
|
||||||
|
tzOffsetMinutes: number
|
||||||
): string {
|
): string {
|
||||||
return `statusHistory:${entityType}:${entityId}:${days}`;
|
return `statusHistory:${entityType}:${entityId}:${days}:${tzOffsetMinutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the epoch seconds of the most recent local-calendar-day midnight,
|
||||||
|
// where "local" is defined by tzOffsetMinutes (minutes to ADD to UTC to get
|
||||||
|
// local time, e.g. Australia/Sydney standard time is 600). Defaults to 0
|
||||||
|
// (UTC) so callers that don't pass a timezone keep the original behavior.
|
||||||
|
function localMidnightSec(tzOffsetMinutes: number): number {
|
||||||
|
const localNow = new Date(Date.now() + tzOffsetMinutes * 60_000);
|
||||||
|
localNow.setUTCHours(0, 0, 0, 0);
|
||||||
|
return Math.floor(localNow.getTime() / 1000) - tzOffsetMinutes * 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCachedStatusHistory(
|
export async function getCachedStatusHistory(
|
||||||
entityType: string,
|
entityType: string,
|
||||||
entityId: number,
|
entityId: number,
|
||||||
days: number
|
days: number,
|
||||||
|
tzOffsetMinutes: number = 0
|
||||||
): Promise<StatusHistoryResponse> {
|
): Promise<StatusHistoryResponse> {
|
||||||
const cacheKey = statusHistoryCacheKey(entityType, entityId, days);
|
const cacheKey = statusHistoryCacheKey(
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days,
|
||||||
|
tzOffsetMinutes
|
||||||
|
);
|
||||||
const cached = await cache.get<StatusHistoryResponse>(cacheKey);
|
const cached = await cache.get<StatusHistoryResponse>(cacheKey);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor to UTC midnight so the query window aligns with stable calendar days
|
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
|
||||||
const utcToday = new Date();
|
// window aligns with stable calendar days for the requesting client
|
||||||
utcToday.setUTCHours(0, 0, 0, 0);
|
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
|
||||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
|
||||||
const startSec = todayMidnightSec - days * 86400;
|
const startSec = todayMidnightSec - days * 86400;
|
||||||
|
|
||||||
const events = await logsDb
|
const events = await logsDb
|
||||||
@@ -63,7 +79,8 @@ export async function getCachedStatusHistory(
|
|||||||
const { buckets, totalDowntime } = computeBuckets(
|
const { buckets, totalDowntime } = computeBuckets(
|
||||||
events,
|
events,
|
||||||
days,
|
days,
|
||||||
priorStatus
|
priorStatus,
|
||||||
|
tzOffsetMinutes
|
||||||
);
|
);
|
||||||
const totalWindow = days * 86400;
|
const totalWindow = days * 86400;
|
||||||
const overallUptime =
|
const overallUptime =
|
||||||
@@ -99,11 +116,19 @@ export const statusHistoryQuerySchema = z
|
|||||||
days: z
|
days: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((v) => (v ? parseInt(v, 10) : 90))
|
.transform((v) => (v ? parseInt(v, 10) : 90)),
|
||||||
|
// Minutes to add to UTC to get the requesting client's local time
|
||||||
|
// (e.g. Australia/Sydney standard time is 600). Optional and
|
||||||
|
// defaults to 0 (UTC) so older clients keep the prior behavior.
|
||||||
|
tzOffsetMinutes: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v ? parseInt(v, 10) : 0))
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
z.object({
|
z.object({
|
||||||
days: z.number().int().min(1).max(365)
|
days: z.number().int().min(1).max(365),
|
||||||
|
tzOffsetMinutes: z.number().int().min(-720).max(840)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -133,15 +158,15 @@ export function computeBuckets(
|
|||||||
id: number;
|
id: number;
|
||||||
}[],
|
}[],
|
||||||
days: number,
|
days: number,
|
||||||
priorStatus: string | null = null
|
priorStatus: string | null = null,
|
||||||
|
tzOffsetMinutes: number = 0
|
||||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
|
// Anchor bucket boundaries to local midnight (UTC when tzOffsetMinutes is
|
||||||
// and don't drift as the cache expires and is recomputed
|
// 0) so dates are stable calendar days for the requesting client and
|
||||||
const utcToday = new Date();
|
// don't drift as the cache expires and is recomputed
|
||||||
utcToday.setUTCHours(0, 0, 0, 0);
|
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
|
||||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
|
||||||
|
|
||||||
const buckets: StatusHistoryDayBucket[] = [];
|
const buckets: StatusHistoryDayBucket[] = [];
|
||||||
let totalDowntime = 0;
|
let totalDowntime = 0;
|
||||||
@@ -237,7 +262,13 @@ export function computeBuckets(
|
|||||||
)
|
)
|
||||||
: 100;
|
: 100;
|
||||||
|
|
||||||
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
|
// Shift by the client's offset before formatting so the label reflects
|
||||||
|
// their local calendar date rather than the UTC date of dayStartSec
|
||||||
|
const dateStr = new Date(
|
||||||
|
(dayStartSec + tzOffsetMinutes * 60) * 1000
|
||||||
|
)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
const hasAnyData = currentStatus !== null || dayEvents.length > 0;
|
const hasAnyData = currentStatus !== null || dayEvents.length > 0;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
db,
|
db,
|
||||||
Org,
|
Org,
|
||||||
orgs,
|
orgs,
|
||||||
|
resourceAccessToken,
|
||||||
resources,
|
resources,
|
||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
@@ -83,6 +84,15 @@ export async function removeUserFromOrg(
|
|||||||
.delete(userOrgs)
|
.delete(userOrgs)
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(resourceAccessToken)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceAccessToken.userId, userId),
|
||||||
|
eq(resourceAccessToken.orgId, org.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await trx.delete(userResources).where(
|
await trx.delete(userResources).where(
|
||||||
and(
|
and(
|
||||||
eq(userResources.userId, userId),
|
eq(userResources.userId, userId),
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export * from "./verifyApiKeySiteResourceAccess";
|
|||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
export * from "./verifyApiKeyDomainAccess";
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
export * from "./verifyApiKeyResourcePolicyAccess";
|
export * from "./verifyApiKeyResourcePolicyAccess";
|
||||||
|
export * from "./verifyApiKeySiteProvisioningKeyAccess";
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
siteProvisioningKeys,
|
||||||
|
siteProvisioningKeyOrg,
|
||||||
|
apiKeyOrg
|
||||||
|
} from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeySiteProvisioningKeyAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const siteProvisioningKeyId =
|
||||||
|
getFirstString(req.params.siteProvisioningKeyId) ||
|
||||||
|
getFirstString(req.body.siteProvisioningKeyId) ||
|
||||||
|
getFirstString(req.query.siteProvisioningKeyId);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!siteProvisioningKeyId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any site provisioning key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(siteProvisioningKeys)
|
||||||
|
.innerJoin(
|
||||||
|
siteProvisioningKeyOrg,
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
siteProvisioningKeys.siteProvisioningKeyId,
|
||||||
|
siteProvisioningKeyOrg.siteProvisioningKeyId
|
||||||
|
),
|
||||||
|
eq(siteProvisioningKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
siteProvisioningKeys.siteProvisioningKeyId,
|
||||||
|
siteProvisioningKeyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row?.siteProvisioningKeys || !row.siteProvisioningKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Site provisioning key with ID ${siteProvisioningKeyId} not found for organization ${orgId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, row.siteProvisioningKeyOrg.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying site provisioning key access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export enum OpenAPITags {
|
|||||||
OrgIdp = "Identity Provider (Organization Only)",
|
OrgIdp = "Identity Provider (Organization Only)",
|
||||||
Client = "Client",
|
Client = "Client",
|
||||||
ApiKey = "API Key",
|
ApiKey = "API Key",
|
||||||
|
SiteProvisioningKey = "Site Provisioning Key",
|
||||||
Domain = "Domain",
|
Domain = "Domain",
|
||||||
Blueprint = "Blueprint",
|
Blueprint = "Blueprint",
|
||||||
Ssh = "SSH",
|
Ssh = "SSH",
|
||||||
|
|||||||
@@ -55,9 +55,14 @@ export async function getHealthCheckStatusHistory(
|
|||||||
|
|
||||||
const entityType = "health_check";
|
const entityType = "health_check";
|
||||||
const entityId = parsedParams.data.healthCheckId;
|
const entityId = parsedParams.data.healthCheckId;
|
||||||
const { days } = parsedQuery.data;
|
const { days, tzOffsetMinutes } = parsedQuery.data;
|
||||||
|
|
||||||
const data = await getCachedStatusHistory(entityType, entityId, days);
|
const data = await getCachedStatusHistory(
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days,
|
||||||
|
tzOffsetMinutes
|
||||||
|
);
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ import {
|
|||||||
resourceRules,
|
resourceRules,
|
||||||
resourcePolicyRules,
|
resourcePolicyRules,
|
||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
roles
|
roles,
|
||||||
|
resourceAccessToken
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
||||||
import { alias } from "@server/db";
|
import { alias } from "@server/db";
|
||||||
@@ -81,11 +82,16 @@ import config from "@server/lib/config";
|
|||||||
import { exchangeSession } from "@server/routers/badger";
|
import { exchangeSession } from "@server/routers/badger";
|
||||||
import {
|
import {
|
||||||
ResourceSessionValidationResult,
|
ResourceSessionValidationResult,
|
||||||
|
createResourceSession,
|
||||||
|
serializeResourceSessionCookie,
|
||||||
validateResourceSessionToken
|
validateResourceSessionToken
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||||
|
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||||
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
@@ -178,6 +184,73 @@ const validateResourceAccessTokenBodySchema = z.strictObject({
|
|||||||
accessToken: z.string()
|
accessToken: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createAccessTokenSessionParamsSchema = z.strictObject({
|
||||||
|
resourceId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAccessTokenSessionBodySchema = z.strictObject({
|
||||||
|
accessTokenId: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccessTokenParamsSchema = z.strictObject({
|
||||||
|
accessTokenId: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
const logAccessAuditBodySchema = z.strictObject({
|
||||||
|
action: z.boolean(),
|
||||||
|
type: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
resourceId: z.number().optional(),
|
||||||
|
siteResourceId: z.number().optional(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
username: z.string(),
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiKey: z
|
||||||
|
.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
apiKeyId: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
metadata: z.any().optional(),
|
||||||
|
userAgent: z.string().optional(),
|
||||||
|
requestIp: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type AccessTokenUserData = {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
role: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resolveAccessTokenUserData(
|
||||||
|
userId: string,
|
||||||
|
orgId: string
|
||||||
|
): Promise<AccessTokenUserData | undefined> {
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||||
|
return {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Certificates by domains query validation
|
// Certificates by domains query validation
|
||||||
const getCertificatesByDomainsQuerySchema = z.strictObject({
|
const getCertificatesByDomainsQuerySchema = z.strictObject({
|
||||||
// Accept domains as string or array (domains or domains[])
|
// Accept domains as string or array (domains or domains[])
|
||||||
@@ -1829,8 +1902,23 @@ hybridRouter.post(
|
|||||||
resourceId
|
resourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let userData: AccessTokenUserData | undefined;
|
||||||
|
if (
|
||||||
|
result.valid &&
|
||||||
|
result.tokenItem?.userId &&
|
||||||
|
result.tokenItem.orgId
|
||||||
|
) {
|
||||||
|
userData = await resolveAccessTokenUserData(
|
||||||
|
result.tokenItem.userId,
|
||||||
|
result.tokenItem.orgId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: result,
|
data: {
|
||||||
|
...result,
|
||||||
|
userData
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: result.valid
|
message: result.valid
|
||||||
@@ -1850,6 +1938,233 @@ hybridRouter.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create a resource session from a valid access token (for remote nodes)
|
||||||
|
hybridRouter.post(
|
||||||
|
"/resource/:resourceId/session/create-access-token",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = createAccessTokenSessionParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = createAccessTokenSessionBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourceId } = parsedParams.data;
|
||||||
|
const { accessTokenId } = parsedBody.data;
|
||||||
|
|
||||||
|
const [tokenItem] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tokenItem || tokenItem.resourceId !== resourceId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Access token not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenItem.persistSession) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Access token does not allow session persistence"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [resource] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!resource || !resource.fullDomain) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateSessionToken();
|
||||||
|
const sess = await createResourceSession({
|
||||||
|
resourceId: resource.resourceId,
|
||||||
|
token,
|
||||||
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
sessionLength: tokenItem.sessionLength,
|
||||||
|
expiresAt: tokenItem.expiresAt,
|
||||||
|
doNotExtend: tokenItem.expiresAt ? true : false
|
||||||
|
});
|
||||||
|
|
||||||
|
const cookieName = config.getRawConfig().server.session_cookie_name;
|
||||||
|
const cookie = serializeResourceSessionCookie(
|
||||||
|
cookieName,
|
||||||
|
resource.fullDomain,
|
||||||
|
token,
|
||||||
|
!resource.ssl,
|
||||||
|
new Date(sess.expiresAt)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: { cookie },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access token session created successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to create access token session"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve access token metadata for remote nodes (cookie session path)
|
||||||
|
hybridRouter.get(
|
||||||
|
"/resource/access-token/:accessTokenId",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = getAccessTokenParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessTokenId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [tokenItem] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tokenItem) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Access token not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData: AccessTokenUserData | undefined;
|
||||||
|
if (tokenItem.userId) {
|
||||||
|
userData = await resolveAccessTokenUserData(
|
||||||
|
tokenItem.userId,
|
||||||
|
tokenItem.orgId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: { tokenItem, userData },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access token retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get access token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access audit log from remote nodes
|
||||||
|
hybridRouter.post(
|
||||||
|
"/logs/access",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedBody = logAccessAuditBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteExitNode = req.remoteExitNode;
|
||||||
|
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Remote exit node not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
await checkExitNodeOrg(
|
||||||
|
remoteExitNode.exitNodeId,
|
||||||
|
parsedBody.data.orgId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Exit node not allowed for this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await logAccessAudit(parsedBody.data);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access audit log saved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to save access audit log"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const geoIpLookupParamsSchema = z.object({
|
const geoIpLookupParamsSchema = z.object({
|
||||||
ip: z.union([z.ipv4(), z.ipv6()])
|
ip: z.union([z.ipv4(), z.ipv6()])
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import * as org from "#private/routers/org";
|
|||||||
import * as logs from "#private/routers/auditLogs";
|
import * as logs from "#private/routers/auditLogs";
|
||||||
import * as alertEvents from "#private/routers/alertEvents";
|
import * as alertEvents from "#private/routers/alertEvents";
|
||||||
import * as certificates from "#private/routers/certificates";
|
import * as certificates from "#private/routers/certificates";
|
||||||
|
import * as siteProvisioning from "#private/routers/siteProvisioning";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyApiKeyHasAction,
|
verifyApiKeyHasAction,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
verifyApiKeyIdpAccess,
|
verifyApiKeyIdpAccess,
|
||||||
verifyApiKeyRoleAccess,
|
verifyApiKeyRoleAccess,
|
||||||
verifyApiKeyUserAccess,
|
verifyApiKeyUserAccess,
|
||||||
|
verifyApiKeySiteProvisioningKeyAccess,
|
||||||
verifyLimits
|
verifyLimits
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import * as user from "#private/routers/user";
|
import * as user from "#private/routers/user";
|
||||||
@@ -215,3 +217,45 @@ authenticated.delete(
|
|||||||
logActionAudit(ActionsEnum.removeUserRole),
|
logActionAudit(ActionsEnum.removeUserRole),
|
||||||
user.removeUserRole
|
user.removeUserRole
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/site-provisioning-key",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createSiteProvisioningKey),
|
||||||
|
logActionAudit(ActionsEnum.createSiteProvisioningKey),
|
||||||
|
siteProvisioning.createSiteProvisioningKey
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site-provisioning-keys",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listSiteProvisioningKeys),
|
||||||
|
siteProvisioning.listSiteProvisioningKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteProvisioningKeyAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteSiteProvisioningKey),
|
||||||
|
logActionAudit(ActionsEnum.deleteSiteProvisioningKey),
|
||||||
|
siteProvisioning.deleteSiteProvisioningKey
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.patch(
|
||||||
|
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteProvisioningKeyAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateSiteProvisioningKey),
|
||||||
|
logActionAudit(ActionsEnum.updateSiteProvisioningKey),
|
||||||
|
siteProvisioning.updateSiteProvisioningKey
|
||||||
|
);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import {
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
|
import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
|
||||||
|
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -34,10 +36,17 @@ const paramsSchema = z.object({
|
|||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
maxBatchSize: z.union([
|
maxBatchSize: z
|
||||||
z.null(),
|
.union([
|
||||||
z.coerce.number().int().positive().max(1_000_000)
|
z.null(),
|
||||||
]),
|
z.coerce.number().int().positive().max(1_000_000)
|
||||||
|
])
|
||||||
|
.openapi({
|
||||||
|
type: "number",
|
||||||
|
default: null,
|
||||||
|
description:
|
||||||
|
"Maximum number of sites that can be provisioned in a single batch. If null, there is no limit."
|
||||||
|
}),
|
||||||
validUntil: z.string().max(255).optional(),
|
validUntil: z.string().max(255).optional(),
|
||||||
approveNewSites: z.boolean().optional().default(true)
|
approveNewSites: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
@@ -57,6 +66,49 @@ const bodySchema = z
|
|||||||
|
|
||||||
export type CreateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
|
export type CreateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
|
const CreateSiteProvisioningKeyResponseDataSchema = z.object({
|
||||||
|
siteProvisioningKeyId: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
siteProvisioningKey: z.string(),
|
||||||
|
lastChars: z.string(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
lastUsed: z.string().nullable(),
|
||||||
|
maxBatchSize: z.number().nullable(),
|
||||||
|
numUsed: z.number(),
|
||||||
|
validUntil: z.string().nullable(),
|
||||||
|
approveNewSites: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/org/{orgId}/site-provisioning-key",
|
||||||
|
description: "Create a new site provisioning key for the organization.",
|
||||||
|
tags: [OpenAPITags.SiteProvisioningKey],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: "Successful response",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: createApiResponseSchema(
|
||||||
|
CreateSiteProvisioningKeyResponseDataSchema
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export async function createSiteProvisioningKey(
|
export async function createSiteProvisioningKey(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -13,23 +13,46 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
|
||||||
db,
|
|
||||||
siteProvisioningKeyOrg,
|
|
||||||
siteProvisioningKeys
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
siteProvisioningKeyId: z.string().nonempty(),
|
siteProvisioningKeyId: z.string().nonempty(),
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "delete",
|
||||||
|
path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}",
|
||||||
|
description: "Delete a site provisioning key.",
|
||||||
|
tags: [OpenAPITags.SiteProvisioningKey],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
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 deleteSiteProvisioningKey(
|
export async function deleteSiteProvisioningKey(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -11,11 +11,7 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
|
||||||
db,
|
|
||||||
siteProvisioningKeyOrg,
|
|
||||||
siteProvisioningKeys
|
|
||||||
} from "@server/db";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -25,6 +21,8 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
|
import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
|
||||||
|
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -45,11 +43,55 @@ const querySchema = z.object({
|
|||||||
.pipe(z.int().nonnegative())
|
.pipe(z.int().nonnegative())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ListSiteProvisioningKeysResponseDataSchema = z.object({
|
||||||
|
siteProvisioningKeys: z.array(
|
||||||
|
z.object({
|
||||||
|
siteProvisioningKeyId: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
lastChars: z.string(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
lastUsed: z.string().nullable(),
|
||||||
|
maxBatchSize: z.number().nullable(),
|
||||||
|
numUsed: z.number(),
|
||||||
|
validUntil: z.string().nullable(),
|
||||||
|
approveNewSites: z.boolean()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
pagination: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
offset: z.number()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/site-provisioning-keys",
|
||||||
|
description: "List all site provisioning keys for an organization.",
|
||||||
|
tags: [OpenAPITags.SiteProvisioningKey],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
query: querySchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: createApiResponseSchema(
|
||||||
|
ListSiteProvisioningKeysResponseDataSchema
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function querySiteProvisioningKeys(orgId: string) {
|
function querySiteProvisioningKeys(orgId: string) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
siteProvisioningKeyId:
|
siteProvisioningKeyId: siteProvisioningKeys.siteProvisioningKeyId,
|
||||||
siteProvisioningKeys.siteProvisioningKeyId,
|
|
||||||
orgId: siteProvisioningKeyOrg.orgId,
|
orgId: siteProvisioningKeyOrg.orgId,
|
||||||
lastChars: siteProvisioningKeys.lastChars,
|
lastChars: siteProvisioningKeys.lastChars,
|
||||||
createdAt: siteProvisioningKeys.createdAt,
|
createdAt: siteProvisioningKeys.createdAt,
|
||||||
|
|||||||
@@ -13,11 +13,7 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
|
||||||
db,
|
|
||||||
siteProvisioningKeyOrg,
|
|
||||||
siteProvisioningKeys
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -25,6 +21,8 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
|
import type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
|
||||||
|
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
siteProvisioningKeyId: z.string().nonempty(),
|
siteProvisioningKeyId: z.string().nonempty(),
|
||||||
@@ -38,7 +36,13 @@ const bodySchema = z
|
|||||||
z.null(),
|
z.null(),
|
||||||
z.coerce.number().int().positive().max(1_000_000)
|
z.coerce.number().int().positive().max(1_000_000)
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
type: "number",
|
||||||
|
default: null,
|
||||||
|
description:
|
||||||
|
"Maximum number of sites that can be provisioned in a single batch. If null, there is no limit."
|
||||||
|
}),
|
||||||
validUntil: z.string().max(255).optional(),
|
validUntil: z.string().max(255).optional(),
|
||||||
approveNewSites: z.boolean().optional()
|
approveNewSites: z.boolean().optional()
|
||||||
})
|
})
|
||||||
@@ -50,7 +54,8 @@ const bodySchema = z
|
|||||||
) {
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "Provide maxBatchSize and/or validUntil and/or approveNewSites",
|
message:
|
||||||
|
"Provide maxBatchSize and/or validUntil and/or approveNewSites",
|
||||||
path: ["maxBatchSize"]
|
path: ["maxBatchSize"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,48 @@ const bodySchema = z
|
|||||||
|
|
||||||
export type UpdateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
|
export type UpdateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
|
const UpdateSiteProvisioningKeyResponseDataSchema = z.object({
|
||||||
|
siteProvisioningKeyId: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
lastChars: z.string(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
lastUsed: z.string().nullable(),
|
||||||
|
maxBatchSize: z.number().nullable(),
|
||||||
|
numUsed: z.number(),
|
||||||
|
validUntil: z.string().nullable(),
|
||||||
|
approveNewSites: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "patch",
|
||||||
|
path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}",
|
||||||
|
description: "Update a site provisioning key.",
|
||||||
|
tags: [OpenAPITags.SiteProvisioningKey],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: createApiResponseSchema(
|
||||||
|
UpdateSiteProvisioningKeyResponseDataSchema
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export async function updateSiteProvisioningKey(
|
export async function updateSiteProvisioningKey(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { hash } from "@node-rs/argon2";
|
|
||||||
import {
|
import {
|
||||||
generateId,
|
generateId,
|
||||||
generateIdFromEntropySize,
|
generateIdFromEntropySize,
|
||||||
@@ -8,18 +7,18 @@ import { db } from "@server/db";
|
|||||||
import {
|
import {
|
||||||
ResourceAccessToken,
|
ResourceAccessToken,
|
||||||
resourceAccessToken,
|
resourceAccessToken,
|
||||||
resources
|
resources,
|
||||||
|
userOrgs
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { createDate, TimeSpan } from "oslo";
|
import { createDate, TimeSpan } from "oslo";
|
||||||
import { hashPassword } from "@server/auth/password";
|
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -28,7 +27,9 @@ export const generateAccessTokenBodySchema = z.strictObject({
|
|||||||
validForSeconds: z.int().positive().optional(), // seconds
|
validForSeconds: z.int().positive().optional(), // seconds
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
description: z.string().optional()
|
description: z.string().optional(),
|
||||||
|
persistSession: z.boolean().optional().default(false),
|
||||||
|
userId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const generateAccssTokenParamsSchema = z.strictObject({
|
export const generateAccssTokenParamsSchema = z.strictObject({
|
||||||
@@ -101,7 +102,14 @@ export async function generateAccessToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { resourceId } = parsedParams.data;
|
const { resourceId } = parsedParams.data;
|
||||||
const { validForSeconds, title, path, description } = parsedBody.data;
|
const {
|
||||||
|
validForSeconds,
|
||||||
|
title,
|
||||||
|
path,
|
||||||
|
description,
|
||||||
|
persistSession,
|
||||||
|
userId
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -112,6 +120,28 @@ export async function generateAccessToken(
|
|||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const [membership] = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.orgId, resource.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"User is not a member of this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionLength = validForSeconds
|
const sessionLength = validForSeconds
|
||||||
? validForSeconds * 1000
|
? validForSeconds * 1000
|
||||||
@@ -133,23 +163,27 @@ export async function generateAccessToken(
|
|||||||
accessTokenId: id,
|
accessTokenId: id,
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
resourceId,
|
resourceId,
|
||||||
|
userId: userId || null,
|
||||||
tokenHash,
|
tokenHash,
|
||||||
expiresAt: expiresAt || null,
|
expiresAt: expiresAt || null,
|
||||||
sessionLength: sessionLength,
|
sessionLength: sessionLength,
|
||||||
title: title || null,
|
title: title || null,
|
||||||
path: path || null,
|
path: path || null,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
|
persistSession,
|
||||||
createdAt: new Date().getTime()
|
createdAt: new Date().getTime()
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
accessTokenId: resourceAccessToken.accessTokenId,
|
accessTokenId: resourceAccessToken.accessTokenId,
|
||||||
orgId: resourceAccessToken.orgId,
|
orgId: resourceAccessToken.orgId,
|
||||||
resourceId: resourceAccessToken.resourceId,
|
resourceId: resourceAccessToken.resourceId,
|
||||||
|
userId: resourceAccessToken.userId,
|
||||||
expiresAt: resourceAccessToken.expiresAt,
|
expiresAt: resourceAccessToken.expiresAt,
|
||||||
sessionLength: resourceAccessToken.sessionLength,
|
sessionLength: resourceAccessToken.sessionLength,
|
||||||
title: resourceAccessToken.title,
|
title: resourceAccessToken.title,
|
||||||
path: resourceAccessToken.path,
|
path: resourceAccessToken.path,
|
||||||
description: resourceAccessToken.description,
|
description: resourceAccessToken.description,
|
||||||
|
persistSession: resourceAccessToken.persistSession,
|
||||||
createdAt: resourceAccessToken.createdAt
|
createdAt: resourceAccessToken.createdAt
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import {
|
|||||||
userResources,
|
userResources,
|
||||||
roleResources,
|
roleResources,
|
||||||
resourceAccessToken,
|
resourceAccessToken,
|
||||||
sites
|
sites,
|
||||||
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
|
import { sql, eq, or, inArray, and, count, isNull, gt } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
@@ -55,11 +56,16 @@ function queryAccessTokens(
|
|||||||
accessTokenId: resourceAccessToken.accessTokenId,
|
accessTokenId: resourceAccessToken.accessTokenId,
|
||||||
orgId: resourceAccessToken.orgId,
|
orgId: resourceAccessToken.orgId,
|
||||||
resourceId: resourceAccessToken.resourceId,
|
resourceId: resourceAccessToken.resourceId,
|
||||||
|
userId: resourceAccessToken.userId,
|
||||||
|
userName: users.name,
|
||||||
|
username: users.username,
|
||||||
|
userEmail: users.email,
|
||||||
sessionLength: resourceAccessToken.sessionLength,
|
sessionLength: resourceAccessToken.sessionLength,
|
||||||
expiresAt: resourceAccessToken.expiresAt,
|
expiresAt: resourceAccessToken.expiresAt,
|
||||||
tokenHash: resourceAccessToken.tokenHash,
|
tokenHash: resourceAccessToken.tokenHash,
|
||||||
title: resourceAccessToken.title,
|
title: resourceAccessToken.title,
|
||||||
description: resourceAccessToken.description,
|
description: resourceAccessToken.description,
|
||||||
|
persistSession: resourceAccessToken.persistSession,
|
||||||
createdAt: resourceAccessToken.createdAt,
|
createdAt: resourceAccessToken.createdAt,
|
||||||
resourceName: resources.name,
|
resourceName: resources.name,
|
||||||
resourceNiceId: resources.niceId,
|
resourceNiceId: resources.niceId,
|
||||||
@@ -75,6 +81,7 @@ function queryAccessTokens(
|
|||||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||||
|
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(
|
inArray(
|
||||||
@@ -97,6 +104,7 @@ function queryAccessTokens(
|
|||||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||||
|
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(
|
inArray(
|
||||||
|
|||||||
@@ -16,18 +16,26 @@ export async function logout(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { user, session } = await verifySession(req);
|
const { user, session } = await verifySession(req);
|
||||||
|
const isSecure = req.protocol === "https";
|
||||||
|
|
||||||
|
// Always clear the session cookie so logout is idempotent, even when
|
||||||
|
// the session is already missing or invalid
|
||||||
|
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||||
|
|
||||||
if (!user || !session) {
|
if (!user || !session) {
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Log out failed because missing or invalid session. IP: ${req.ip}.`
|
`Log out with missing or invalid session. IP: ${req.ip}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return next(
|
|
||||||
createHttpError(
|
return response<null>(res, {
|
||||||
HttpCode.BAD_REQUEST,
|
data: null,
|
||||||
"You must be logged in to sign out"
|
success: true,
|
||||||
)
|
error: false,
|
||||||
);
|
message: "Logged out successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -37,9 +45,6 @@ export async function logout(
|
|||||||
logger.error("Failed to invalidate session", error);
|
logger.error("Failed to invalidate session", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSecure = req.protocol === "https";
|
|
||||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export async function requestPasswordReset(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
|
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(email)}&token=${token}`;
|
||||||
|
|
||||||
if (!config.getRawConfig().email) {
|
if (!config.getRawConfig().email) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
import {
|
||||||
|
createResourceSession,
|
||||||
|
serializeResourceSessionCookie,
|
||||||
|
validateResourceSessionToken
|
||||||
|
} from "@server/auth/sessions/resource";
|
||||||
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import {
|
import {
|
||||||
getResourceByDomain,
|
getResourceByDomain,
|
||||||
@@ -13,6 +18,7 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
Org,
|
Org,
|
||||||
Resource,
|
Resource,
|
||||||
|
ResourceAccessToken,
|
||||||
ResourceHeaderAuth,
|
ResourceHeaderAuth,
|
||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -21,7 +27,10 @@ import {
|
|||||||
ResourcePolicyPassword,
|
ResourcePolicyPassword,
|
||||||
ResourcePolicyHeaderAuth,
|
ResourcePolicyHeaderAuth,
|
||||||
ResourceRule,
|
ResourceRule,
|
||||||
ResourceSession
|
ResourceSession,
|
||||||
|
db,
|
||||||
|
resourceAccessToken,
|
||||||
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||||
@@ -41,11 +50,13 @@ import {
|
|||||||
enforceResourceSessionLength
|
enforceResourceSessionLength
|
||||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logRequestAudit } from "./logRequestAudit";
|
import { logRequestAudit } from "./logRequestAudit";
|
||||||
|
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
||||||
import { REGIONS } from "@server/db/regions";
|
import { REGIONS } from "@server/db/regions";
|
||||||
import { localCache } from "#dynamic/lib/cache";
|
import { localCache } from "#dynamic/lib/cache";
|
||||||
import { APP_VERSION } from "@server/lib/consts";
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string(), z.string()).optional(),
|
sessions: z.record(z.string(), z.string()).optional(),
|
||||||
@@ -350,22 +361,15 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (valid && tokenItem) {
|
if (valid && tokenItem) {
|
||||||
logRequestAudit(
|
return await allowAccessToken(
|
||||||
{
|
res,
|
||||||
action: true,
|
resource,
|
||||||
reason: 102, // valid access token
|
tokenItem,
|
||||||
resourceId: resource.resourceId,
|
sessions,
|
||||||
orgId: resource.orgId,
|
dontStripSession,
|
||||||
location: ipCC,
|
parsedBody.data,
|
||||||
apiKey: {
|
ipCC
|
||||||
name: tokenItem.title,
|
|
||||||
apiKeyId: tokenItem.accessTokenId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parsedBody.data
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return allowed(res, undefined, dontStripSession);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,22 +405,15 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (valid && tokenItem) {
|
if (valid && tokenItem) {
|
||||||
logRequestAudit(
|
return await allowAccessToken(
|
||||||
{
|
res,
|
||||||
action: true,
|
resource,
|
||||||
reason: 102, // valid access token
|
tokenItem,
|
||||||
resourceId: resource.resourceId,
|
sessions,
|
||||||
orgId: resource.orgId,
|
dontStripSession,
|
||||||
location: ipCC,
|
parsedBody.data,
|
||||||
apiKey: {
|
ipCC
|
||||||
name: tokenItem.title,
|
|
||||||
apiKeyId: tokenItem.accessTokenId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parsedBody.data
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return allowed(res, undefined, dontStripSession);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,22 +663,37 @@ export async function verifyResourceSession(
|
|||||||
"Resource allowed because access token session is valid"
|
"Resource allowed because access token session is valid"
|
||||||
);
|
);
|
||||||
|
|
||||||
logRequestAudit(
|
const [tokenItem] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceAccessToken.accessTokenId,
|
||||||
|
resourceSession.accessTokenId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const userData = tokenItem
|
||||||
|
? await getAccessTokenUserData(
|
||||||
|
tokenItem,
|
||||||
|
resource.orgId
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
logAccessTokenRequestAudit(
|
||||||
{
|
{
|
||||||
action: true,
|
|
||||||
reason: 102, // valid access token
|
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
location: ipCC,
|
location: ipCC,
|
||||||
apiKey: {
|
accessTokenId: resourceSession.accessTokenId,
|
||||||
name: null,
|
tokenTitle: tokenItem?.title ?? null,
|
||||||
apiKeyId: resourceSession.accessTokenId
|
userData
|
||||||
}
|
|
||||||
},
|
},
|
||||||
parsedBody.data
|
parsedBody.data
|
||||||
);
|
);
|
||||||
|
|
||||||
return allowed(res, undefined, dontStripSession);
|
return allowed(res, userData, dontStripSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceSession.userSessionId && sso) {
|
if (resourceSession.userSessionId && sso) {
|
||||||
@@ -892,6 +904,227 @@ function allowed(
|
|||||||
return response<VerifyUserResponse>(res, data);
|
return response<VerifyUserResponse>(res, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function allowAccessToken(
|
||||||
|
res: Response,
|
||||||
|
resource: Resource,
|
||||||
|
tokenItem: ResourceAccessToken,
|
||||||
|
sessions: Record<string, string> | undefined,
|
||||||
|
dontStripSession: boolean | undefined,
|
||||||
|
auditBody: VerifyResourceSessionSchema,
|
||||||
|
location?: string
|
||||||
|
) {
|
||||||
|
const userData = await getAccessTokenUserData(tokenItem, resource.orgId);
|
||||||
|
|
||||||
|
logAccessTokenRequestAudit(
|
||||||
|
{
|
||||||
|
resourceId: resource.resourceId,
|
||||||
|
orgId: resource.orgId,
|
||||||
|
location,
|
||||||
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
tokenTitle: tokenItem.title,
|
||||||
|
userData
|
||||||
|
},
|
||||||
|
auditBody
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tokenItem.persistSession) {
|
||||||
|
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||||
|
return allowed(res, userData, dontStripSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceSessionToken = extractResourceSessionToken(
|
||||||
|
sessions ?? {},
|
||||||
|
resource.ssl
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resourceSessionToken) {
|
||||||
|
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||||
|
let resourceSession: ResourceSession | null | undefined =
|
||||||
|
localCache.get(sessionCacheKey);
|
||||||
|
|
||||||
|
if (!resourceSession) {
|
||||||
|
const result = await validateResourceSessionToken(
|
||||||
|
resourceSessionToken,
|
||||||
|
resource.resourceId
|
||||||
|
);
|
||||||
|
resourceSession = result?.resourceSession;
|
||||||
|
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
resourceSession &&
|
||||||
|
!resourceSession.isRequestToken &&
|
||||||
|
resourceSession.accessTokenId === tokenItem.accessTokenId
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
"Resource allowed because existing access token session is valid"
|
||||||
|
);
|
||||||
|
return allowed(res, userData, dontStripSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||||
|
return await createAccessTokenSession(res, resource, tokenItem, userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAccessTokenSession(
|
||||||
|
res: Response,
|
||||||
|
resource: Resource,
|
||||||
|
tokenItem: ResourceAccessToken,
|
||||||
|
userData?: BasicUserData
|
||||||
|
) {
|
||||||
|
const token = generateSessionToken();
|
||||||
|
const sess = await createResourceSession({
|
||||||
|
resourceId: resource.resourceId,
|
||||||
|
token,
|
||||||
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
sessionLength: tokenItem.sessionLength,
|
||||||
|
expiresAt: tokenItem.expiresAt,
|
||||||
|
doNotExtend: tokenItem.expiresAt ? true : false
|
||||||
|
});
|
||||||
|
const cookieName = config.getRawConfig().server.session_cookie_name;
|
||||||
|
const cookie = serializeResourceSessionCookie(
|
||||||
|
cookieName,
|
||||||
|
resource.fullDomain!,
|
||||||
|
token,
|
||||||
|
!resource.ssl,
|
||||||
|
new Date(sess.expiresAt)
|
||||||
|
);
|
||||||
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
logger.debug("Access token is valid, creating new session");
|
||||||
|
return allowed(res, userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessTokenUserData(
|
||||||
|
tokenItem: ResourceAccessToken,
|
||||||
|
orgId: string
|
||||||
|
): Promise<BasicUserData | undefined> {
|
||||||
|
if (!tokenItem.userId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `accessTokenUser:${tokenItem.userId}:${orgId}`;
|
||||||
|
const cached = localCache.get(cacheKey) as BasicUserData | null | undefined;
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, tokenItem.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
localCache.set(cacheKey, null, 5);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||||
|
const userData: BasicUserData = {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||||
|
};
|
||||||
|
|
||||||
|
localCache.set(cacheKey, userData, 12);
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAccessTokenRequestAudit(
|
||||||
|
data: {
|
||||||
|
resourceId: number;
|
||||||
|
orgId: string;
|
||||||
|
location?: string;
|
||||||
|
accessTokenId: string;
|
||||||
|
tokenTitle: string | null;
|
||||||
|
userData?: BasicUserData;
|
||||||
|
},
|
||||||
|
body: VerifyResourceSessionSchema
|
||||||
|
) {
|
||||||
|
if (data.userData) {
|
||||||
|
logRequestAudit(
|
||||||
|
{
|
||||||
|
action: true,
|
||||||
|
reason: 102, // valid access token
|
||||||
|
resourceId: data.resourceId,
|
||||||
|
orgId: data.orgId,
|
||||||
|
location: data.location,
|
||||||
|
user: {
|
||||||
|
username: data.userData.username,
|
||||||
|
userId: data.userData.userId
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
accessTokenId: data.accessTokenId,
|
||||||
|
accessTokenTitle: data.tokenTitle
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequestAudit(
|
||||||
|
{
|
||||||
|
action: true,
|
||||||
|
reason: 102, // valid access token
|
||||||
|
resourceId: data.resourceId,
|
||||||
|
orgId: data.orgId,
|
||||||
|
location: data.location,
|
||||||
|
apiKey: {
|
||||||
|
name: data.tokenTitle,
|
||||||
|
apiKeyId: data.accessTokenId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAccessTokenAccessAudit(
|
||||||
|
tokenItem: ResourceAccessToken,
|
||||||
|
resource: Resource,
|
||||||
|
userData: BasicUserData | undefined,
|
||||||
|
body: VerifyResourceSessionSchema
|
||||||
|
) {
|
||||||
|
const userAgent =
|
||||||
|
body.headers?.["user-agent"] || body.headers?.["User-Agent"];
|
||||||
|
|
||||||
|
if (userData) {
|
||||||
|
logAccessAudit({
|
||||||
|
orgId: resource.orgId,
|
||||||
|
resourceId: resource.resourceId,
|
||||||
|
action: true,
|
||||||
|
type: "accessToken",
|
||||||
|
user: {
|
||||||
|
username: userData.username,
|
||||||
|
userId: userData.userId
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
accessTokenTitle: tokenItem.title
|
||||||
|
},
|
||||||
|
userAgent,
|
||||||
|
requestIp: body.requestIp
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logAccessAudit({
|
||||||
|
orgId: resource.orgId,
|
||||||
|
resourceId: resource.resourceId,
|
||||||
|
action: true,
|
||||||
|
type: "accessToken",
|
||||||
|
apiKey: {
|
||||||
|
name: tokenItem.title,
|
||||||
|
apiKeyId: tokenItem.accessTokenId
|
||||||
|
},
|
||||||
|
userAgent,
|
||||||
|
requestIp: body.requestIp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function headerAuthChallenged(
|
async function headerAuthChallenged(
|
||||||
res: Response,
|
res: Response,
|
||||||
redirectPath?: string,
|
redirectPath?: string,
|
||||||
|
|||||||
@@ -809,16 +809,6 @@ authenticated.post(
|
|||||||
accessToken.generateAccessToken
|
accessToken.generateAccessToken
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/session-token`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
|
|
||||||
logActionAudit(ActionsEnum.createResourceSessionToken),
|
|
||||||
resource.createResourceSessionToken
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
`/access-token/:accessTokenId`,
|
`/access-token/:accessTokenId`,
|
||||||
verifyApiKeyAccessTokenAccess,
|
verifyApiKeyAccessTokenAccess,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { publicKey, port, chainId } = message.data;
|
const { publicKey, port, localEndpoints, chainId } = message.data;
|
||||||
const siteId = newt.siteId;
|
const siteId = newt.siteId;
|
||||||
|
|
||||||
// Get the current site data
|
// Get the current site data
|
||||||
@@ -69,7 +69,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
.update(sites)
|
.update(sites)
|
||||||
.set({
|
.set({
|
||||||
publicKey,
|
publicKey,
|
||||||
listenPort: port
|
listenPort: port,
|
||||||
|
localEndpoints: localEndpoints
|
||||||
|
? JSON.stringify(localEndpoints)
|
||||||
|
: null
|
||||||
})
|
})
|
||||||
.where(eq(sites.siteId, siteId))
|
.where(eq(sites.siteId, siteId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
siteId: number;
|
siteId: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
|
localEndpoints?: string[];
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
serverIP?: string | null;
|
serverIP?: string | null;
|
||||||
serverPort?: number | null;
|
serverPort?: number | null;
|
||||||
@@ -206,6 +207,9 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
name: site.name,
|
name: site.name,
|
||||||
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
|
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
|
||||||
endpoint: site.endpoint,
|
endpoint: site.endpoint,
|
||||||
|
localEndpoints: site.localEndpoints
|
||||||
|
? JSON.parse(site.localEndpoints)
|
||||||
|
: undefined,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort,
|
serverPort: site.listenPort,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { db, sites } from "@server/db";
|
||||||
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
|
import { clients, Olm } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export const handleOlmLocalMessage: MessageHandler = async (context) => {
|
||||||
|
const { message, client: c, sendToClient } = context;
|
||||||
|
const olm = c as Olm;
|
||||||
|
|
||||||
|
logger.info("Handling local olm message!");
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn("Olm not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!olm.clientId) {
|
||||||
|
logger.warn("Olm has no client!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = olm.clientId;
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.warn("Client not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
|
||||||
|
if (!client.pubKey) {
|
||||||
|
logger.warn("Client has no endpoint or listen port");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteId, chainId } = message.data;
|
||||||
|
|
||||||
|
// Get the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site || !site.exitNodeId) {
|
||||||
|
logger.warn("Site not found or has no exit node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the peer on the newt
|
||||||
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
|
endpoint: "" // this removes the endpoint so the newt knows to accept local
|
||||||
|
});
|
||||||
|
|
||||||
|
// Just ack the message, we don't keep sending it
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/local",
|
||||||
|
data: {
|
||||||
|
siteId: siteId,
|
||||||
|
chainId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false,
|
||||||
|
excludeSender: false
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -79,9 +79,9 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// update the peer on the exit node
|
// update the peer on the newt
|
||||||
await newtUpdatePeer(siteId, client.pubKey, {
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
endpoint: "" // this removes the endpoint so the exit node knows to relay
|
endpoint: "" // this removes the endpoint so the newt knows to relay
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,24 +3,15 @@ import {
|
|||||||
db,
|
db,
|
||||||
networks,
|
networks,
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
siteResources,
|
siteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import {
|
import { clients, clientSitesAssociationsCache, Olm, sites } from "@server/db";
|
||||||
clients,
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
Olm,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import { generateAliasConfig } from "@server/lib/ip";
|
||||||
generateAliasConfig,
|
|
||||||
} from "@server/lib/ip";
|
|
||||||
import { generateRemoteSubnets } from "@server/lib/ip";
|
import { generateRemoteSubnets } from "@server/lib/ip";
|
||||||
import {
|
import { addPeer as newtAddPeer } from "@server/routers/newt/peers";
|
||||||
addPeer as newtAddPeer,
|
|
||||||
} from "@server/routers/newt/peers";
|
|
||||||
|
|
||||||
export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||||
context
|
context
|
||||||
@@ -135,10 +126,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
clientSiteResourcesAssociationsCache.siteResourceId
|
clientSiteResourcesAssociationsCache.siteResourceId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.innerJoin(
|
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
|
||||||
networks,
|
|
||||||
eq(siteResources.networkId, networks.networkId)
|
|
||||||
)
|
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
and(
|
and(
|
||||||
@@ -147,10 +135,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
eq(
|
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
|
||||||
clientSiteResourcesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return connect message with all site configurations
|
// Return connect message with all site configurations
|
||||||
@@ -161,6 +146,9 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
name: site.name,
|
name: site.name,
|
||||||
endpoint: site.endpoint,
|
endpoint: site.endpoint,
|
||||||
|
localEndpoints: site.localEndpoints
|
||||||
|
? JSON.parse(site.localEndpoints)
|
||||||
|
: undefined,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort,
|
serverPort: site.listenPort,
|
||||||
@@ -170,7 +158,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
aliases: generateAliasConfig(
|
aliases: generateAliasConfig(
|
||||||
allSiteResources.map(({ siteResources }) => siteResources)
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
),
|
),
|
||||||
chainId: chainId,
|
chainId: chainId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { db, exitNodes, sites } from "@server/db";
|
||||||
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
|
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export const handleOlmUnLocalMessage: MessageHandler = async (context) => {
|
||||||
|
const { message, client: c, sendToClient } = context;
|
||||||
|
const olm = c as Olm;
|
||||||
|
|
||||||
|
logger.info("Handling unlocal olm message!");
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn("Olm not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!olm.clientId) {
|
||||||
|
logger.warn("Olm has no client!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = olm.clientId;
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.warn("Client not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
|
||||||
|
if (!client.pubKey) {
|
||||||
|
logger.warn("Client has no endpoint or listen port");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteId, chainId } = message.data;
|
||||||
|
|
||||||
|
// Get the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn("Site not found or has no exit node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [clientSiteAssociation] = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, olm.clientId),
|
||||||
|
eq(clientSitesAssociationsCache.siteId, siteId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!clientSiteAssociation) {
|
||||||
|
logger.warn("Client-Site association not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientSiteAssociation.endpoint) {
|
||||||
|
logger.warn("Client-Site association has no endpoint, cannot unrelay");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the peer on the newt
|
||||||
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
|
endpoint: clientSiteAssociation.isRelayed
|
||||||
|
? ""
|
||||||
|
: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/unlocal",
|
||||||
|
data: {
|
||||||
|
siteId: siteId,
|
||||||
|
chainId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false,
|
||||||
|
excludeSender: false
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -77,9 +77,9 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the peer on the exit node
|
// update the peer on the newt
|
||||||
await newtUpdatePeer(siteId, client.pubKey, {
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node
|
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ export * from "./recoverOlmWithFingerprint";
|
|||||||
export * from "./handleOlmDisconnectingMessage";
|
export * from "./handleOlmDisconnectingMessage";
|
||||||
export * from "./handleOlmServerInitAddPeerHandshake";
|
export * from "./handleOlmServerInitAddPeerHandshake";
|
||||||
export * from "./offlineChecker";
|
export * from "./offlineChecker";
|
||||||
|
export * from "./handleOlmUnLocalMessage";
|
||||||
|
export * from "./handleOlmLocalMessage";
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export async function addPeer(
|
|||||||
serverPort: number | null;
|
serverPort: number | null;
|
||||||
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
|
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
|
localEndpoints?: string[]; // optional, list of local endpoints for the peer
|
||||||
},
|
},
|
||||||
olmId?: string,
|
olmId?: string,
|
||||||
version?: string | null
|
version?: string | null
|
||||||
@@ -44,6 +45,7 @@ export async function addPeer(
|
|||||||
name: peer.name,
|
name: peer.name,
|
||||||
publicKey: peer.publicKey,
|
publicKey: peer.publicKey,
|
||||||
endpoint: peer.endpoint,
|
endpoint: peer.endpoint,
|
||||||
|
localEndpoints: peer.localEndpoints,
|
||||||
relayEndpoint: peer.relayEndpoint,
|
relayEndpoint: peer.relayEndpoint,
|
||||||
serverIP: peer.serverIP,
|
serverIP: peer.serverIP,
|
||||||
serverPort: peer.serverPort,
|
serverPort: peer.serverPort,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { Resource, resources } from "@server/db";
|
import { Resource, resources, users } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -156,11 +156,42 @@ export async function authWithAccessToken(
|
|||||||
doNotExtend: true
|
doNotExtend: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let accessAuditUser: { username: string; userId: string } | undefined;
|
||||||
|
if (tokenItem.userId) {
|
||||||
|
const [associatedUser] = await db
|
||||||
|
.select({
|
||||||
|
userId: users.userId,
|
||||||
|
username: users.username
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, tokenItem.userId))
|
||||||
|
.limit(1);
|
||||||
|
if (associatedUser) {
|
||||||
|
accessAuditUser = {
|
||||||
|
userId: associatedUser.userId,
|
||||||
|
username: associatedUser.username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logAccessAudit({
|
logAccessAudit({
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
action: true,
|
action: true,
|
||||||
type: "accessToken",
|
type: "accessToken",
|
||||||
|
apiKey: accessAuditUser
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
name: tokenItem.title,
|
||||||
|
apiKeyId: tokenItem.accessTokenId
|
||||||
|
},
|
||||||
|
user: accessAuditUser,
|
||||||
|
metadata: accessAuditUser
|
||||||
|
? {
|
||||||
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
accessTokenTitle: tokenItem.title
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
userAgent: req.headers["user-agent"],
|
userAgent: req.headers["user-agent"],
|
||||||
requestIp: req.ip
|
requestIp: req.ip
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { resources, users, userOrgs } from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
|
||||||
import { response } from "@server/lib/response";
|
|
||||||
|
|
||||||
const createResourceSessionTokenParams = z.strictObject({
|
|
||||||
resourceId: z.coerce.number().int().positive()
|
|
||||||
});
|
|
||||||
|
|
||||||
const createResourceSessionTokenBody = z.strictObject({
|
|
||||||
userId: z.string().nonempty(),
|
|
||||||
idpId: z.coerce.number().int().positive().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateResourceSessionTokenResponse = {
|
|
||||||
requestToken: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createResourceSessionToken(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = createResourceSessionTokenParams.safeParse(
|
|
||||||
req.params
|
|
||||||
);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedBody = createResourceSessionTokenBody.safeParse(req.body);
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { resourceId } = parsedParams.data;
|
|
||||||
const { userId, idpId } = parsedBody.data;
|
|
||||||
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidates = await db
|
|
||||||
.select({ userId: users.userId })
|
|
||||||
.from(userOrgs)
|
|
||||||
.innerJoin(users, eq(userOrgs.userId, users.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(users.userId, userId),
|
|
||||||
eq(userOrgs.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`User not found in the organization that owns this resource`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.length > 1) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Multiple users match this username (external users from different identity providers). Specify idpId to disambiguate."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUserId = candidates[0].userId;
|
|
||||||
|
|
||||||
const appSessionToken = generateSessionToken();
|
|
||||||
const appSession = await createSession(appSessionToken, targetUserId);
|
|
||||||
|
|
||||||
const requestToken = generateSessionToken();
|
|
||||||
await createResourceSession({
|
|
||||||
resourceId,
|
|
||||||
token: requestToken,
|
|
||||||
userSessionId: appSession.sessionId,
|
|
||||||
isRequestToken: true,
|
|
||||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
|
||||||
sessionLength: 1000 * 30,
|
|
||||||
doNotExtend: true
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug("Resource session token created successfully");
|
|
||||||
|
|
||||||
return response<CreateResourceSessionTokenResponse>(res, {
|
|
||||||
data: { requestToken },
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Resource session token created successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,9 +42,14 @@ export async function getResourceStatusHistory(
|
|||||||
|
|
||||||
const entityType = "resource";
|
const entityType = "resource";
|
||||||
const entityId = parsedParams.data.resourceId;
|
const entityId = parsedParams.data.resourceId;
|
||||||
const { days } = parsedQuery.data;
|
const { days, tzOffsetMinutes } = parsedQuery.data;
|
||||||
|
|
||||||
const data = await getCachedStatusHistory(entityType, entityId, days);
|
const data = await getCachedStatusHistory(
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days,
|
||||||
|
tzOffsetMinutes
|
||||||
|
);
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export * from "./getResourceWhitelist";
|
|||||||
export * from "./authWithWhitelist";
|
export * from "./authWithWhitelist";
|
||||||
export * from "./authWithAccessToken";
|
export * from "./authWithAccessToken";
|
||||||
export * from "./getExchangeToken";
|
export * from "./getExchangeToken";
|
||||||
export * from "./createResourceSessionToken";
|
|
||||||
export * from "./createResourceRule";
|
export * from "./createResourceRule";
|
||||||
export * from "./deleteResourceRule";
|
export * from "./deleteResourceRule";
|
||||||
export * from "./listResourceRules";
|
export * from "./listResourceRules";
|
||||||
|
|||||||
@@ -42,9 +42,14 @@ export async function getSiteStatusHistory(
|
|||||||
|
|
||||||
const entityType = "site";
|
const entityType = "site";
|
||||||
const entityId = parsedParams.data.siteId;
|
const entityId = parsedParams.data.siteId;
|
||||||
const { days } = parsedQuery.data;
|
const { days, tzOffsetMinutes } = parsedQuery.data;
|
||||||
|
|
||||||
const data = await getCachedStatusHistory(entityType, entityId, days);
|
const data = await getCachedStatusHistory(
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days,
|
||||||
|
tzOffsetMinutes
|
||||||
|
);
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export async function adminGeneratePasswordResetCode(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${existingUser[0].email}&token=${token}`;
|
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(existingUser[0].email!)}&token=${token}`;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Admin generated password reset code for user ${existingUser[0].email} (${userId})`
|
`Admin generated password reset code for user ${existingUser[0].email} (${userId})`
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export async function inviteUser(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
||||||
|
|
||||||
if (doEmail) {
|
if (doEmail) {
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
@@ -341,7 +341,7 @@ export async function inviteUser(
|
|||||||
.values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
|
.values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
|
||||||
});
|
});
|
||||||
|
|
||||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
||||||
|
|
||||||
if (doEmail) {
|
if (doEmail) {
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import {
|
|||||||
handleOlmServerPeerAddMessage,
|
handleOlmServerPeerAddMessage,
|
||||||
handleOlmUnRelayMessage,
|
handleOlmUnRelayMessage,
|
||||||
handleOlmDisconnectingMessage,
|
handleOlmDisconnectingMessage,
|
||||||
handleOlmServerInitAddPeerHandshake
|
handleOlmServerInitAddPeerHandshake,
|
||||||
|
handleOlmLocalMessage,
|
||||||
|
handleOlmUnLocalMessage
|
||||||
} from "../olm";
|
} from "../olm";
|
||||||
import { handleHealthcheckStatusMessage } from "../target";
|
import { handleHealthcheckStatusMessage } from "../target";
|
||||||
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
||||||
@@ -32,6 +34,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
|||||||
"olm/wg/register": handleOlmRegisterMessage,
|
"olm/wg/register": handleOlmRegisterMessage,
|
||||||
"olm/wg/relay": handleOlmRelayMessage,
|
"olm/wg/relay": handleOlmRelayMessage,
|
||||||
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
||||||
|
"olm/wg/local": handleOlmLocalMessage,
|
||||||
|
"olm/wg/unlocal": handleOlmUnLocalMessage,
|
||||||
"olm/ping": handleOlmPingMessage,
|
"olm/ping": handleOlmPingMessage,
|
||||||
"olm/disconnecting": handleOlmDisconnectingMessage,
|
"olm/disconnecting": handleOlmDisconnectingMessage,
|
||||||
"newt/disconnecting": handleNewtDisconnectingMessage,
|
"newt/disconnecting": handleNewtDisconnectingMessage,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import m18 from "./scriptsPg/1.18.3";
|
|||||||
import m19 from "./scriptsPg/1.18.4";
|
import m19 from "./scriptsPg/1.18.4";
|
||||||
import m20 from "./scriptsPg/1.19.0";
|
import m20 from "./scriptsPg/1.19.0";
|
||||||
import m21 from "./scriptsPg/1.20.0";
|
import m21 from "./scriptsPg/1.20.0";
|
||||||
|
import m22 from "./scriptsPg/1.21.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -53,7 +54,8 @@ const migrations = [
|
|||||||
{ version: "1.18.3", run: m18 },
|
{ version: "1.18.3", run: m18 },
|
||||||
{ version: "1.18.4", run: m19 },
|
{ version: "1.18.4", run: m19 },
|
||||||
{ version: "1.19.0", run: m20 },
|
{ version: "1.19.0", run: m20 },
|
||||||
{ version: "1.20.0", run: m21 }
|
{ version: "1.20.0", run: m21 },
|
||||||
|
{ version: "1.21.0", run: m22 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import m40 from "./scriptsSqlite/1.18.4";
|
|||||||
import m41 from "./scriptsSqlite/1.19.0";
|
import m41 from "./scriptsSqlite/1.19.0";
|
||||||
import m42 from "./scriptsSqlite/1.19.1";
|
import m42 from "./scriptsSqlite/1.19.1";
|
||||||
import m43 from "./scriptsSqlite/1.20.0";
|
import m43 from "./scriptsSqlite/1.20.0";
|
||||||
|
import m44 from "./scriptsSqlite/1.21.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -89,7 +90,8 @@ const migrations = [
|
|||||||
{ version: "1.18.4", run: m40 },
|
{ version: "1.18.4", run: m40 },
|
||||||
{ version: "1.19.0", run: m41 },
|
{ version: "1.19.0", run: m41 },
|
||||||
{ version: "1.19.1", run: m42 },
|
{ version: "1.19.1", run: m42 },
|
||||||
{ version: "1.20.0", run: m43 }
|
{ version: "1.20.0", run: m43 },
|
||||||
|
{ version: "1.21.0", run: m44 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import path from "path";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const version = "1.21.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "resourceAccessToken" ADD COLUMN "userId" varchar;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "resourceAccessToken" ADD COLUMN "persistSession" boolean DEFAULT false NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "resources" ADD COLUMN "status" varchar DEFAULT 'approved';
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "siteResources" ADD COLUMN "status" varchar DEFAULT 'approved';
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "sites" ADD COLUMN "localEndpoints" varchar;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "resourceAccessToken" ADD CONSTRAINT "resourceAccessToken_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const traefikPath = path.join(
|
||||||
|
APP_PATH,
|
||||||
|
"traefik",
|
||||||
|
"traefik_config.yml"
|
||||||
|
);
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
experimental: z.object({
|
||||||
|
plugins: z.object({
|
||||||
|
badger: z.object({
|
||||||
|
moduleName: z.string(),
|
||||||
|
version: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
|
||||||
|
const traefikConfig = yaml.load(traefikFileContents) as any;
|
||||||
|
|
||||||
|
const parsedConfig = schema.safeParse(traefikConfig);
|
||||||
|
|
||||||
|
if (!parsedConfig.success) {
|
||||||
|
throw new Error(fromZodError(parsedConfig.error).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
traefikConfig.experimental.plugins.badger.version = "v1.5.0";
|
||||||
|
|
||||||
|
const updatedTraefikYaml = yaml.dump(traefikConfig);
|
||||||
|
|
||||||
|
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Updated the version of Badger in your Traefik configuration to v1.5.0"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information."
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import path from "path";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const version = "1.21.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'resourceAccessToken' ADD 'userId' text REFERENCES user(id);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'resourceAccessToken' ADD 'persistSession' integer DEFAULT false NOT NULL;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'resources' ADD 'status' text DEFAULT 'approved';
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'siteResources' ADD 'status' text DEFAULT 'approved';
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'sites' ADD 'localEndpoints' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const traefikPath = path.join(
|
||||||
|
APP_PATH,
|
||||||
|
"traefik",
|
||||||
|
"traefik_config.yml"
|
||||||
|
);
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
experimental: z.object({
|
||||||
|
plugins: z.object({
|
||||||
|
badger: z.object({
|
||||||
|
moduleName: z.string(),
|
||||||
|
version: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
|
||||||
|
const traefikConfig = yaml.load(traefikFileContents) as any;
|
||||||
|
|
||||||
|
const parsedConfig = schema.safeParse(traefikConfig);
|
||||||
|
|
||||||
|
if (!parsedConfig.success) {
|
||||||
|
throw new Error(fromZodError(parsedConfig.error).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
traefikConfig.experimental.plugins.badger.version = "v1.5.0";
|
||||||
|
|
||||||
|
const updatedTraefikYaml = yaml.dump(traefikConfig);
|
||||||
|
|
||||||
|
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Updated the version of Badger in your Traefik configuration to v1.5.0"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information."
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -248,6 +248,39 @@ export async function loginProxy(
|
|||||||
return await makeApiRequest<LoginResponse>(url, "POST", request);
|
return await makeApiRequest<LoginResponse>(url, "POST", request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function logoutProxy(): Promise<ResponseT<null>> {
|
||||||
|
const env = pullEnv();
|
||||||
|
const serverPort = process.env.SERVER_EXTERNAL_PORT;
|
||||||
|
const url = `http://localhost:${serverPort}/api/v1/auth/logout`;
|
||||||
|
|
||||||
|
const result = await makeApiRequest<null>(url, "POST");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headersList = await reqHeaders();
|
||||||
|
const host = headersList.get("host")?.split(":")[0];
|
||||||
|
const allCookies = await cookies();
|
||||||
|
const clearOptions = {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "lax" as const,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 0
|
||||||
|
};
|
||||||
|
// Clear both host-only and domain-scoped variants.
|
||||||
|
allCookies.set(env.server.sessionCookieName, "", clearOptions);
|
||||||
|
if (host) {
|
||||||
|
allCookies.set(env.server.sessionCookieName, "", {
|
||||||
|
...clearOptions,
|
||||||
|
domain: host
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (cookieError) {
|
||||||
|
console.error("Failed to clear session cookie:", cookieError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function securityKeyStartProxy(
|
export async function securityKeyStartProxy(
|
||||||
request: SecurityKeyStartRequest,
|
request: SecurityKeyStartRequest,
|
||||||
forceLogin?: boolean
|
forceLogin?: boolean
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import { ChevronsUpDown } from "lucide-react";
|
|||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
||||||
import { constructShareLink } from "@app/lib/shareLinks";
|
import { constructShareLink } from "@app/lib/shareLinks";
|
||||||
import { ShareLinkRow } from "@app/components/ShareLinksTable";
|
|
||||||
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -63,11 +62,26 @@ import AccessTokenSection from "@app/components/AccessTokenUsage";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toUnicode } from "punycode";
|
import { toUnicode } from "punycode";
|
||||||
import { ResourceSelector, type SelectedResource } from "./resource-selector";
|
import { ResourceSelector, type SelectedResource } from "./resource-selector";
|
||||||
|
import { UserSelector, type SelectedUser } from "@app/components/user-selector";
|
||||||
|
|
||||||
|
type CreatedShareLink = {
|
||||||
|
accessTokenId: string;
|
||||||
|
resourceId: number;
|
||||||
|
resourceName: string;
|
||||||
|
resourceNiceId: string;
|
||||||
|
title: string | null;
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number | null;
|
||||||
|
userId?: string | null;
|
||||||
|
userName?: string | null;
|
||||||
|
username?: string | null;
|
||||||
|
userEmail?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
onCreated?: (result: ShareLinkRow) => void;
|
onCreated?: (result: CreatedShareLink) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CreateShareLinkForm({
|
export default function CreateShareLinkForm({
|
||||||
@@ -85,6 +99,8 @@ export default function CreateShareLinkForm({
|
|||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [neverExpire, setNeverExpire] = useState(false);
|
const [neverExpire, setNeverExpire] = useState(false);
|
||||||
|
const [persistSession, setPersistSession] = useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<SelectedUser | null>(null);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -175,7 +191,9 @@ export default function CreateShareLinkForm({
|
|||||||
values.resourceName ||
|
values.resourceName ||
|
||||||
"Resource" + values.resourceId
|
"Resource" + values.resourceId
|
||||||
}),
|
}),
|
||||||
path: values.path
|
path: values.path,
|
||||||
|
persistSession,
|
||||||
|
userId: selectedUser?.id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -205,7 +223,11 @@ export default function CreateShareLinkForm({
|
|||||||
resourceNiceId: selectedResource ? selectedResource.niceId : "",
|
resourceNiceId: selectedResource ? selectedResource.niceId : "",
|
||||||
title: token.title,
|
title: token.title,
|
||||||
createdAt: token.createdAt,
|
createdAt: token.createdAt,
|
||||||
expiresAt: token.expiresAt
|
expiresAt: token.expiresAt,
|
||||||
|
userId: token.userId,
|
||||||
|
userName: selectedUser?.text ?? null,
|
||||||
|
username: null,
|
||||||
|
userEmail: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +242,9 @@ export default function CreateShareLinkForm({
|
|||||||
setOpen(val);
|
setOpen(val);
|
||||||
setLink(null);
|
setLink(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setNeverExpire(false);
|
||||||
|
setPersistSession(false);
|
||||||
|
setSelectedUser(null);
|
||||||
form.reset();
|
form.reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -344,6 +369,48 @@ export default function CreateShareLinkForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"shareAssociateUserOptional"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
!selectedUser &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedUser?.text
|
||||||
|
? selectedUser.text
|
||||||
|
: t("userSelect")}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
||||||
|
<UserSelector
|
||||||
|
orgId={org.org.orgId}
|
||||||
|
selectedUser={
|
||||||
|
selectedUser
|
||||||
|
}
|
||||||
|
onSelectUser={
|
||||||
|
setSelectedUser
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"shareAssociateUserDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -437,6 +504,34 @@ export default function CreateShareLinkForm({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="persist-session"
|
||||||
|
checked={persistSession}
|
||||||
|
onCheckedChange={(val) =>
|
||||||
|
setPersistSession(
|
||||||
|
val as boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="persist-session"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"sharePersistSession"
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"sharePersistSessionDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("shareExpireDescription")}
|
{t("shareExpireDescription")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -93,14 +93,20 @@ export default function InviteStatusCard({
|
|||||||
setType(type);
|
setType(type);
|
||||||
|
|
||||||
if (!user && type === "user_does_not_exist") {
|
if (!user && type === "user_does_not_exist") {
|
||||||
|
const inviteRedirect = encodeURIComponent(
|
||||||
|
`/invite?token=${tokenParam}`
|
||||||
|
);
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}`
|
||||||
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
: `/auth/signup?redirect=${inviteRedirect}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else if (!user && type === "not_logged_in") {
|
} else if (!user && type === "not_logged_in") {
|
||||||
|
const inviteRedirect = encodeURIComponent(
|
||||||
|
`/invite?token=${tokenParam}`
|
||||||
|
);
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=${inviteRedirect}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -112,17 +118,23 @@ export default function InviteStatusCard({
|
|||||||
|
|
||||||
async function goToLogin() {
|
async function goToLogin() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
|
const inviteRedirect = encodeURIComponent(
|
||||||
|
`/invite?token=${tokenParam}`
|
||||||
|
);
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=${inviteRedirect}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goToSignup() {
|
async function goToSignup() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
|
const inviteRedirect = encodeURIComponent(
|
||||||
|
`/invite?token=${tokenParam}`
|
||||||
|
);
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}`
|
||||||
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
: `/auth/signup?redirect=${inviteRedirect}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { Shield, ArrowRight } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { useState } from "react";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { logoutProxy } from "@app/actions/server";
|
||||||
|
|
||||||
type OrgPolicyRequiredProps = {
|
type OrgPolicyRequiredProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -40,21 +40,23 @@ export default function OrgPolicyRequired({
|
|||||||
}: OrgPolicyRequiredProps) {
|
}: OrgPolicyRequiredProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
|
|
||||||
const sessionExpired =
|
const sessionExpired =
|
||||||
policies?.maxSessionLength &&
|
policies?.maxSessionLength &&
|
||||||
policies.maxSessionLength.compliant === false;
|
policies.maxSessionLength.compliant === false;
|
||||||
|
|
||||||
function reauthenticate() {
|
async function reauthenticate() {
|
||||||
api.post("/auth/logout")
|
setLoading(true);
|
||||||
.catch(() => {})
|
try {
|
||||||
.then(() => {
|
await logoutProxy();
|
||||||
const destination = redirectAfterAuth ?? `/${orgId}`;
|
} catch (error) {
|
||||||
router.push(destination);
|
console.error("Error during logout:", error);
|
||||||
router.refresh();
|
} finally {
|
||||||
});
|
const destination = redirectAfterAuth ?? `/${orgId}`;
|
||||||
|
router.push(destination);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionExpired) {
|
if (sessionExpired) {
|
||||||
@@ -76,6 +78,7 @@ export default function OrgPolicyRequired({
|
|||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={reauthenticate}
|
onClick={reauthenticate}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t("reauthenticate")}
|
{t("reauthenticate")}
|
||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ function getActionsCategories(root: boolean) {
|
|||||||
Logs: {
|
Logs: {
|
||||||
[t("actionExportLogs")]: "exportLogs",
|
[t("actionExportLogs")]: "exportLogs",
|
||||||
[t("actionViewLogs")]: "viewLogs"
|
[t("actionViewLogs")]: "viewLogs"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Site Provisioning Key": {
|
||||||
|
[t("actionCreateSiteProvisioningKey")]: "createSiteProvisioningKey",
|
||||||
|
[t("actionListSiteProvisioningKeys")]: "listSiteProvisioningKeys",
|
||||||
|
[t("actionUpdateSiteProvisioningKey")]: "updateSiteProvisioningKey",
|
||||||
|
[t("actionDeleteSiteProvisioningKey")]: "deleteSiteProvisioningKey"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import moment from "moment";
|
|||||||
import CreateShareLinkForm from "@app/components/CreateShareLinkForm";
|
import CreateShareLinkForm from "@app/components/CreateShareLinkForm";
|
||||||
import { constructShareLink } from "@app/lib/shareLinks";
|
import { constructShareLink } from "@app/lib/shareLinks";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
|
|
||||||
export type ShareLinkRow = {
|
export type ShareLinkRow = {
|
||||||
accessTokenId: string;
|
accessTokenId: string;
|
||||||
@@ -44,6 +45,10 @@ export type ShareLinkRow = {
|
|||||||
title: string | null;
|
title: string | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
|
userId?: string | null;
|
||||||
|
userName?: string | null;
|
||||||
|
username?: string | null;
|
||||||
|
userEmail?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ShareLinksTableProps = {
|
type ShareLinksTableProps = {
|
||||||
@@ -155,6 +160,41 @@ export default function ShareLinksTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "userId",
|
||||||
|
friendlyName: t("user"),
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("user")}
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
if (!r.userId) {
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={`/${orgId}/settings/access/users/${r.userId}`}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{getUserDisplayName({
|
||||||
|
email: r.userEmail,
|
||||||
|
name: r.userName,
|
||||||
|
username: r.username
|
||||||
|
})}
|
||||||
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// accessorKey: "domain",
|
// accessorKey: "domain",
|
||||||
// header: "Link",
|
// header: "Link",
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "./ui/command";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import type { SelectedUser } from "./users-selector";
|
||||||
|
|
||||||
|
export type { SelectedUser };
|
||||||
|
|
||||||
|
export type UserSelectorProps = {
|
||||||
|
orgId: string;
|
||||||
|
selectedUser?: SelectedUser | null;
|
||||||
|
onSelectUser: (user: SelectedUser | null) => void;
|
||||||
|
allowClear?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UserSelector({
|
||||||
|
orgId,
|
||||||
|
selectedUser,
|
||||||
|
onSelectUser,
|
||||||
|
allowClear = true
|
||||||
|
}: UserSelectorProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [userSearchQuery, setUserSearchQuery] = useState("");
|
||||||
|
const [debouncedValue] = useDebounce(userSearchQuery, 150);
|
||||||
|
|
||||||
|
const { data: users = [] } = useQuery(
|
||||||
|
orgQueries.users({ orgId, perPage: 10, query: debouncedValue })
|
||||||
|
);
|
||||||
|
|
||||||
|
const usersShown = useMemo(() => {
|
||||||
|
const allUsers: Array<SelectedUser> = users.map((u) => ({
|
||||||
|
id: u.id,
|
||||||
|
text: getUserDisplayName(u)
|
||||||
|
}));
|
||||||
|
if (
|
||||||
|
debouncedValue.trim().length === 0 &&
|
||||||
|
selectedUser &&
|
||||||
|
!allUsers.find((user) => user.id === selectedUser.id)
|
||||||
|
) {
|
||||||
|
allUsers.unshift(selectedUser);
|
||||||
|
}
|
||||||
|
return allUsers;
|
||||||
|
}, [users, selectedUser, debouncedValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={t("userSearch")}
|
||||||
|
value={userSearchQuery}
|
||||||
|
onValueChange={setUserSearchQuery}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{t("usersNotFound")}</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{allowClear && (
|
||||||
|
<CommandItem
|
||||||
|
value="__none__"
|
||||||
|
onSelect={() => {
|
||||||
|
onSelectUser(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
!selectedUser ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{t("none")}
|
||||||
|
</CommandItem>
|
||||||
|
)}
|
||||||
|
{usersShown.map((user) => (
|
||||||
|
<CommandItem
|
||||||
|
value={`${user.text}:${user.id}`}
|
||||||
|
key={user.id}
|
||||||
|
onSelect={() => {
|
||||||
|
onSelectUser(user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
user.id === selectedUser?.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{user.text}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
);
|
||||||
|
}
|
||||||
+12
-5
@@ -650,9 +650,13 @@ export const orgQueries = {
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const,
|
queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const tzOffsetMinutes = -new Date().getTimezoneOffset();
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<StatusHistoryResponse>
|
AxiosResponse<StatusHistoryResponse>
|
||||||
>(`/site/${siteId}/status-history?days=${days}`, { signal });
|
>(
|
||||||
|
`/site/${siteId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -667,11 +671,13 @@ export const orgQueries = {
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, days] as const,
|
queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, days] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const tzOffsetMinutes = -new Date().getTimezoneOffset();
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<StatusHistoryResponse>
|
AxiosResponse<StatusHistoryResponse>
|
||||||
>(`/resource/${resourceId}/status-history?days=${days}`, {
|
>(
|
||||||
signal
|
`/resource/${resourceId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
|
||||||
});
|
{ signal }
|
||||||
|
);
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -693,10 +699,11 @@ export const orgQueries = {
|
|||||||
days
|
days
|
||||||
] as const,
|
] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const tzOffsetMinutes = -new Date().getTimezoneOffset();
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<StatusHistoryResponse>
|
AxiosResponse<StatusHistoryResponse>
|
||||||
>(
|
>(
|
||||||
`/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}`,
|
`/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
Reference in New Issue
Block a user