mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-07 00:39:53 +00:00
Compare commits
113 Commits
1.18.3-s.1
...
resource-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b3656fad | ||
|
|
54c1dd3bae | ||
|
|
a8f4d2b7d1 | ||
|
|
51f1693dbd | ||
|
|
b33a6e6fac | ||
|
|
fc2c13a686 | ||
|
|
f4602a120e | ||
|
|
7ccceeea0d | ||
|
|
f81f78f294 | ||
|
|
6cab223f12 | ||
|
|
7b05c02508 | ||
|
|
5922bfb1a0 | ||
|
|
43f2e32231 | ||
|
|
20ebdc6289 | ||
|
|
a80ae49a33 | ||
|
|
660197eef1 | ||
|
|
f3eb823bc3 | ||
|
|
61c13db090 | ||
|
|
ccbd793f52 | ||
|
|
d13e6896a8 | ||
|
|
83a36ead10 | ||
|
|
b61b74b0b5 | ||
|
|
01b068c50f | ||
|
|
fee44ce960 | ||
|
|
1906504a86 | ||
|
|
36bcba332c | ||
|
|
304ab1964c | ||
|
|
b286096c7b | ||
|
|
a22a4b6e74 | ||
|
|
9a680d2374 | ||
|
|
f80e212b07 | ||
|
|
8a39b3fd45 | ||
|
|
61ec938b00 | ||
|
|
6686de6788 | ||
|
|
79636cbb30 | ||
|
|
2fa1bc6cdc | ||
|
|
c5f6d822ca | ||
|
|
4de4bf9625 | ||
|
|
5d956080f2 | ||
|
|
f8e18de2fc | ||
|
|
884482ec35 | ||
|
|
9b43948fa4 | ||
|
|
bcd6cd99cc | ||
|
|
37ceba6b81 | ||
|
|
dfe42e9016 | ||
|
|
38aa2dace8 | ||
|
|
136c3eff0c | ||
|
|
642999c8b1 | ||
|
|
c5fc49b4fa | ||
|
|
cd5a38b1eb | ||
|
|
595842c2c9 | ||
|
|
82d5276ade | ||
|
|
51eb782831 | ||
|
|
de2980e1bc | ||
|
|
8a3c0d9a08 | ||
|
|
1a5e9f1005 | ||
|
|
f42c013f33 | ||
|
|
42c9bda939 | ||
|
|
cbce9fae3a | ||
|
|
e44b15ecd5 | ||
|
|
7f6ca31757 | ||
|
|
a1eb248474 | ||
|
|
be2b1fd1ce | ||
|
|
20b65f549e | ||
|
|
1dc8be373c | ||
|
|
22b2e6b3d4 | ||
|
|
89e7107a47 | ||
|
|
0a69131c38 | ||
|
|
590f2c29b3 | ||
|
|
0ddcce6fe1 | ||
|
|
8a54fb7f23 | ||
|
|
5c280b024e | ||
|
|
033cc62ce7 | ||
|
|
4c69b7a64e | ||
|
|
e7ab9b3f37 | ||
|
|
3143662f82 | ||
|
|
18964ba2a3 | ||
|
|
f862404c5c | ||
|
|
c292578f80 | ||
|
|
7b02d4104d | ||
|
|
2ef5d90e13 | ||
|
|
d6a8021613 | ||
|
|
c5231d37f6 | ||
|
|
4d803a40c9 | ||
|
|
1d709b551a | ||
|
|
335411de4c | ||
|
|
0e4abdf4b6 | ||
|
|
267b40b73c | ||
|
|
ba9a0c5e3c | ||
|
|
9e0b7ff0d7 | ||
|
|
003bf7fdf3 | ||
|
|
c3fdda026b | ||
|
|
a53363d064 | ||
|
|
ee21e1faa7 | ||
|
|
e409a34a09 | ||
|
|
7177ab7f77 | ||
|
|
801f6fb661 | ||
|
|
805d82b8d9 | ||
|
|
bd6d790495 | ||
|
|
2305163474 | ||
|
|
dda53dcb16 | ||
|
|
2c3e768867 | ||
|
|
8d682ed9ad | ||
|
|
47fe497ca1 | ||
|
|
4d5f364663 | ||
|
|
c3db8b972f | ||
|
|
cfced63ba1 | ||
|
|
51aa55f963 | ||
|
|
e7df24841e | ||
|
|
e6fd4c32c4 | ||
|
|
f6590aedbd | ||
|
|
3cb9e02533 | ||
|
|
4d792350ef |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,9 +17,9 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite*
|
||||
!Dockerfile.sqlite
|
||||
*.sqlite3
|
||||
*.sqlite3*
|
||||
*.log
|
||||
.machinelogs*.json
|
||||
*-audit.json
|
||||
@@ -54,3 +54,4 @@ hydrateSaas.ts
|
||||
CLAUDE.md
|
||||
drizzle.config.ts
|
||||
server/setup/migrations.ts
|
||||
solo.yml
|
||||
@@ -1,28 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, certificates } from "@server/db";
|
||||
|
||||
type ClearCertificatesArgs = {};
|
||||
|
||||
export const clearCertificates: CommandModule<{}, ClearCertificatesArgs> = {
|
||||
command: "clear-certificates",
|
||||
describe: "Delete all entries from the certificates table",
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
console.log("Clearing all certificates from the database...");
|
||||
|
||||
const deleted = await db.delete(certificates).returning();
|
||||
|
||||
console.log(
|
||||
`Deleted ${deleted.length} certificate(s) from the database`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||
import { deleteClient } from "./commands/deleteClient";
|
||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||
import { clearCertificates } from "./commands/clearCertificates";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
@@ -20,6 +19,5 @@ yargs(hideBin(process.argv))
|
||||
.command(clearLicenseKeys)
|
||||
.command(deleteClient)
|
||||
.command(generateOrgCaKeys)
|
||||
.command(clearCertificates)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "Няма валидни методи за удостоверение",
|
||||
"ip": "IP",
|
||||
"reason": "Причина",
|
||||
"requestLogs": "Логове за HTTP заявки",
|
||||
"requestLogs": "Заявка за логове",
|
||||
"requestAnalytics": "Анализи На Заявки",
|
||||
"host": "Хост",
|
||||
"location": "Местоположение",
|
||||
"actionLogs": "Дневници на действията",
|
||||
"sidebarLogsRequest": "Логове за HTTP заявки",
|
||||
"sidebarLogsRequest": "Заявка за логове",
|
||||
"sidebarLogsAccess": "Достъп до логове",
|
||||
"sidebarLogsAction": "Дневници на действията",
|
||||
"logRetention": "Задържане на логове",
|
||||
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
||||
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
||||
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
||||
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
|
||||
"logRetentionRequestLabel": "Задържане на логове на заявки",
|
||||
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
||||
"logRetentionAccessLabel": "Задържане на логове за достъп",
|
||||
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
||||
"httpDestConnectionLogsTitle": "Логове на връзката",
|
||||
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
||||
"httpDestRequestLogsTitle": "Логове за HTTP заявки",
|
||||
"httpDestRequestLogsTitle": "Заявки за логове",
|
||||
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
||||
"httpDestSaveChanges": "Запази промените",
|
||||
"httpDestCreateDestination": "Създаване на дестинация",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
|
||||
"domainPickerWildcardCertWarningLink": "Научете повече",
|
||||
"health": "Здраве",
|
||||
"domainPendingErrorTitle": "Проблем при проверка",
|
||||
"memberPortalTitle": "Ресурси",
|
||||
"memberPortalDescription": "Ресурси, до които имате достъп в тази организация",
|
||||
"memberPortalSortBy": "Сортиране по...",
|
||||
"memberPortalSortNameAsc": "Име А-Я",
|
||||
"memberPortalSortNameDesc": "Име Я-А",
|
||||
"memberPortalSortDomainAsc": "Домен А-Я",
|
||||
"memberPortalSortDomainDesc": "Домен Я-А",
|
||||
"memberPortalSortEnabledFirst": "Активирани Първи",
|
||||
"memberPortalSortDisabledFirst": "Деактивирани Първи",
|
||||
"memberPortalRefresh": "Обнови",
|
||||
"memberPortalRefreshResources": "Обнови ресурсите",
|
||||
"memberPortalFailedToLoad": "Грешка при зареждане на ресурсите",
|
||||
"memberPortalFailedToLoadDescription": "Грешка при зареждане на ресурсите. Моля, проверете връзката си и опитайте отново.",
|
||||
"memberPortalUnableToLoad": "Неуспешно зареждане на ресурси",
|
||||
"memberPortalTryAgain": "Опитай отново",
|
||||
"memberPortalNoResourcesFound": "Няма намерени ресурси",
|
||||
"memberPortalNoResourcesAvailable": "Няма налични ресурси",
|
||||
"memberPortalNoResourcesMatchSearch": "Няма ресурси, съвпадащи с \"{query}\". Опитайте да промените търсените условия или нулирайте търсенето, за да видите всички ресурси.",
|
||||
"memberPortalNoResourcesAccess": "Още нямате достъп до ресурси. Свържете се с вашия администратор, за да получите достъп до нужните ресурси.",
|
||||
"memberPortalClearSearch": "Изчисти търсенето",
|
||||
"memberPortalPublicResources": "Публични ресурси",
|
||||
"memberPortalPublicResourcesDescription": "Уеб приложения и услуги, достъпни през браузър",
|
||||
"memberPortalCopiedToClipboard": "Копирано в клипборда",
|
||||
"memberPortalCopiedUrlDescription": "URL адресът на ресурса е копиран в клипборда.",
|
||||
"memberPortalOpenResource": "Отвори ресурса",
|
||||
"memberPortalPrivateResources": "Частни ресурси",
|
||||
"memberPortalPrivateResourcesDescription": "Ресурси на вътрешната мрежа, достъпни чрез клиент",
|
||||
"memberPortalResourceDetails": "Детайли за ресурса",
|
||||
"memberPortalMode": "Режим",
|
||||
"memberPortalDestination": "Дестинация",
|
||||
"memberPortalAlias": "Алиас",
|
||||
"memberPortalCopiedAliasDescription": "Алиасът на ресурса е копиран в клипборда.",
|
||||
"memberPortalCopiedDestinationDescription": "Дестинацията на ресурса е копирана в клипборда.",
|
||||
"memberPortalRequiresClientConnection": "Изисква връзка с клиента",
|
||||
"memberPortalAuthMethods": "Методи на удостоверяване",
|
||||
"memberPortalSso": "Единно вход (SSO)",
|
||||
"memberPortalPasswordProtected": "Защитено с парола",
|
||||
"memberPortalPinCode": "ПИН код",
|
||||
"memberPortalEmailWhitelist": "Бял списък на имейли",
|
||||
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
|
||||
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
|
||||
"memberPortalPrevious": "Предишен",
|
||||
"memberPortalNext": "Следващ"
|
||||
"domainPendingErrorTitle": "Проблем при проверка"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP adresa",
|
||||
"reason": "Důvod",
|
||||
"requestLogs": "Záznamy HTTP požadavků",
|
||||
"requestLogs": "Záznamy požadavků",
|
||||
"requestAnalytics": "Vyžádat analýzu",
|
||||
"host": "Hostitel",
|
||||
"location": "Poloha",
|
||||
"actionLogs": "Záznamy akcí",
|
||||
"sidebarLogsRequest": "Záznamy HTTP požadavků",
|
||||
"sidebarLogsRequest": "Záznamy požadavků",
|
||||
"sidebarLogsAccess": "Protokoly přístupu",
|
||||
"sidebarLogsAction": "Záznamy akcí",
|
||||
"logRetention": "Zaznamenávání záznamu",
|
||||
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
||||
"requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci",
|
||||
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
||||
"logRetentionRequestLabel": "Zachování logu HTTP požadavků",
|
||||
"logRetentionRequestLabel": "Zachování logu žádosti",
|
||||
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
||||
"logRetentionAccessLabel": "Zachování záznamu přístupu",
|
||||
"logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Správní opatření prováděná uživateli v rámci organizace.",
|
||||
"httpDestConnectionLogsTitle": "Protokoly připojení",
|
||||
"httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.",
|
||||
"httpDestRequestLogsTitle": "Záznamy HTTP požadavků",
|
||||
"httpDestRequestLogsTitle": "Záznamy požadavků",
|
||||
"httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.",
|
||||
"httpDestSaveChanges": "Uložit změny",
|
||||
"httpDestCreateDestination": "Vytvořit cíl",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
|
||||
"domainPickerWildcardCertWarningLink": "Zjistit více",
|
||||
"health": "Zdraví",
|
||||
"domainPendingErrorTitle": "Problém s ověřením",
|
||||
"memberPortalTitle": "Zdroje",
|
||||
"memberPortalDescription": "Zdroje, ke kterým máte v této organizaci přístup",
|
||||
"memberPortalSortBy": "Řadit podle...",
|
||||
"memberPortalSortNameAsc": "Názvu A-Z",
|
||||
"memberPortalSortNameDesc": "Názvu Z-A",
|
||||
"memberPortalSortDomainAsc": "Domény A-Z",
|
||||
"memberPortalSortDomainDesc": "Domény Z-A",
|
||||
"memberPortalSortEnabledFirst": "Nejprve povoleno",
|
||||
"memberPortalSortDisabledFirst": "Nejprve zakázáno",
|
||||
"memberPortalRefresh": "Aktualizovat",
|
||||
"memberPortalRefreshResources": "Aktualizovat zdroje",
|
||||
"memberPortalFailedToLoad": "Nepodařilo se načíst zdroje",
|
||||
"memberPortalFailedToLoadDescription": "Nepodařilo se načíst zdroje. Zkontrolujte prosím své připojení a zkuste to znovu.",
|
||||
"memberPortalUnableToLoad": "Nelze načíst zdroje",
|
||||
"memberPortalTryAgain": "Zkusit znovu",
|
||||
"memberPortalNoResourcesFound": "Žádné zdroje nebyly nalezeny",
|
||||
"memberPortalNoResourcesAvailable": "Žádné zdroje nejsou k dispozici",
|
||||
"memberPortalNoResourcesMatchSearch": "Žádné zdroje neodpovídají \"{query}\". Zkuste přizpůsobit své vyhledávací termíny nebo vyčistit hledání, abyste viděli všechny zdroje.",
|
||||
"memberPortalNoResourcesAccess": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého správce, aby vám poskytl přístup k potřebným zdrojům.",
|
||||
"memberPortalClearSearch": "Vymazat hledání",
|
||||
"memberPortalPublicResources": "Veřejné zdroje",
|
||||
"memberPortalPublicResourcesDescription": "Webové aplikace a služby přístupné přes prohlížeč",
|
||||
"memberPortalCopiedToClipboard": "Zkopírováno do schránky",
|
||||
"memberPortalCopiedUrlDescription": "URL zdroje byla zkopírována do vaší schránky.",
|
||||
"memberPortalOpenResource": "Otevřít zdroj",
|
||||
"memberPortalPrivateResources": "Soukromé zdroje",
|
||||
"memberPortalPrivateResourcesDescription": "Interní síťové zdroje přístupné přes klienta",
|
||||
"memberPortalResourceDetails": "Podrobnosti o zdroji",
|
||||
"memberPortalMode": "Režim",
|
||||
"memberPortalDestination": "Cíl",
|
||||
"memberPortalAlias": "Přezdívka",
|
||||
"memberPortalCopiedAliasDescription": "Alias zdroje byl zkopírován do vaší schránky.",
|
||||
"memberPortalCopiedDestinationDescription": "Cíl zdroje byl zkopírován do vaší schránky.",
|
||||
"memberPortalRequiresClientConnection": "Vyžaduje klientské připojení",
|
||||
"memberPortalAuthMethods": "Metody ověřování",
|
||||
"memberPortalSso": "Jedno přihlášení (SSO)",
|
||||
"memberPortalPasswordProtected": "Heslo chráněno",
|
||||
"memberPortalPinCode": "PIN kód",
|
||||
"memberPortalEmailWhitelist": "Seznam povolených emailů",
|
||||
"memberPortalResourceDisabled": "Zdroj je zakázán",
|
||||
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
|
||||
"memberPortalPrevious": "Předchozí",
|
||||
"memberPortalNext": "Následující"
|
||||
"domainPendingErrorTitle": "Problém s ověřením"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "Keine gültige Authentifizierungsmethode verfügbar",
|
||||
"ip": "IP",
|
||||
"reason": "Grund",
|
||||
"requestLogs": "HTTP Anforderungsprotokolle",
|
||||
"requestLogs": "Logs anfordern",
|
||||
"requestAnalytics": "Anfrage-Analyse anzeigen",
|
||||
"host": "Host",
|
||||
"location": "Standort",
|
||||
"actionLogs": "Aktionsprotokolle",
|
||||
"sidebarLogsRequest": "HTTP Anforderungsprotokolle",
|
||||
"sidebarLogsRequest": "Logs anfordern",
|
||||
"sidebarLogsAccess": "Zugriffsprotokolle",
|
||||
"sidebarLogsAction": "Aktionsprotokolle",
|
||||
"logRetention": "Log-Speicherung",
|
||||
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
||||
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
||||
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
||||
"logRetentionRequestLabel": "HTTP Anforderungsprotokoll Aufbewahrung",
|
||||
"logRetentionRequestLabel": "Log-Speicherung anfordern",
|
||||
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
||||
"logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung",
|
||||
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
||||
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
||||
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
||||
"httpDestRequestLogsTitle": "HTTP Anforderungsprotokolle",
|
||||
"httpDestRequestLogsTitle": "Logs anfordern",
|
||||
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
||||
"httpDestSaveChanges": "Änderungen speichern",
|
||||
"httpDestCreateDestination": "Ziel erstellen",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
|
||||
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
|
||||
"health": "Gesundheit",
|
||||
"domainPendingErrorTitle": "Verifizierungsproblem",
|
||||
"memberPortalTitle": "Ressourcen",
|
||||
"memberPortalDescription": "Ressourcen, auf die Sie in dieser Organisation Zugriff haben",
|
||||
"memberPortalSortBy": "Sortieren nach...",
|
||||
"memberPortalSortNameAsc": "Name A-Z",
|
||||
"memberPortalSortNameDesc": "Name Z-A",
|
||||
"memberPortalSortDomainAsc": "Domain A-Z",
|
||||
"memberPortalSortDomainDesc": "Domain Z-A",
|
||||
"memberPortalSortEnabledFirst": "Zuerst aktiviert",
|
||||
"memberPortalSortDisabledFirst": "Zuerst deaktiviert",
|
||||
"memberPortalRefresh": "Aktualisieren",
|
||||
"memberPortalRefreshResources": "Ressourcen aktualisieren",
|
||||
"memberPortalFailedToLoad": "Fehler beim Laden der Ressourcen",
|
||||
"memberPortalFailedToLoadDescription": "Fehler beim Laden der Ressourcen. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
|
||||
"memberPortalUnableToLoad": "Ressourcen konnten nicht geladen werden",
|
||||
"memberPortalTryAgain": "Nochmal versuchen",
|
||||
"memberPortalNoResourcesFound": "Keine Ressourcen gefunden",
|
||||
"memberPortalNoResourcesAvailable": "Keine Ressourcen verfügbar",
|
||||
"memberPortalNoResourcesMatchSearch": "Keine Ressourcen passen zu \"{query}\". Versuchen Sie, Ihre Suchbegriffe anzupassen oder die Suche zu löschen, um alle Ressourcen anzuzeigen.",
|
||||
"memberPortalNoResourcesAccess": "Sie haben noch keinen Zugriff auf Ressourcen. Wenden Sie sich an Ihren Administrator, um Zugriff auf die benötigten Ressourcen zu erhalten.",
|
||||
"memberPortalClearSearch": "Suchverlauf löschen",
|
||||
"memberPortalPublicResources": "Öffentliche Ressourcen",
|
||||
"memberPortalPublicResourcesDescription": "Webanwendungen und Dienste, die über den Browser zugänglich sind",
|
||||
"memberPortalCopiedToClipboard": "In die Zwischenablage kopiert",
|
||||
"memberPortalCopiedUrlDescription": "Ressourcen-URL wurde in Ihre Zwischenablage kopiert.",
|
||||
"memberPortalOpenResource": "Ressource öffnen",
|
||||
"memberPortalPrivateResources": "Private Ressourcen",
|
||||
"memberPortalPrivateResourcesDescription": "Interne Netzwerkressourcen, die über den Client zugänglich sind",
|
||||
"memberPortalResourceDetails": "Ressourcendetails",
|
||||
"memberPortalMode": "Modus",
|
||||
"memberPortalDestination": "Ziel",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "Ressourcenalias wurde in Ihre Zwischenablage kopiert.",
|
||||
"memberPortalCopiedDestinationDescription": "Ressourcenziel wurde in Ihre Zwischenablage kopiert.",
|
||||
"memberPortalRequiresClientConnection": "Erfordert Client-Verbindung",
|
||||
"memberPortalAuthMethods": "Authentifizierungsmethoden",
|
||||
"memberPortalSso": "Single Sign-On (SSO)",
|
||||
"memberPortalPasswordProtected": "Passwortgeschützt",
|
||||
"memberPortalPinCode": "PIN-Code",
|
||||
"memberPortalEmailWhitelist": "E-Mail-Whitelist",
|
||||
"memberPortalResourceDisabled": "Ressource deaktiviert",
|
||||
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
|
||||
"memberPortalPrevious": "Vorherige",
|
||||
"memberPortalNext": "Nächste"
|
||||
"domainPendingErrorTitle": "Verifizierungsproblem"
|
||||
}
|
||||
|
||||
@@ -204,11 +204,33 @@
|
||||
"resourcesSearch": "Search resources...",
|
||||
"resourceAdd": "Add Resource",
|
||||
"resourceErrorDelte": "Error deleting resource",
|
||||
"resourcePoliciesTitle": "Manage Resource Policies",
|
||||
"resourcePoliciesAttachedResourcesColumnTitle": "Attached resources",
|
||||
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
||||
"resourcePoliciesAttachedResourcesEmpty": "no resources",
|
||||
"resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources",
|
||||
"resourcePoliciesSearch": "Search policies...",
|
||||
"resourcePoliciesAdd": "Add Policy",
|
||||
"resourcePoliciesDefaultBadgeText": "Default policy",
|
||||
"resourcePoliciesCreate": "Create Resource Policy",
|
||||
"resourcePoliciesCreateDescription": "Follow the steps below to create a new policy",
|
||||
"resourcePolicyName": "Policy Name",
|
||||
"resourcePolicyNameDescription": "Give this policy a name to identify it across your resources",
|
||||
"resourcePolicyNamePlaceholder": "e.g. Internal Access Policy",
|
||||
"resourcePoliciesSeeAll": "See All Policies",
|
||||
"resourcePolicyAuthMethodAdd": "Add Authentication Method",
|
||||
"resourcePolicyOtpEmailAdd": "Add OTP emails",
|
||||
"resourcePolicyRulesAdd": "Add Rules",
|
||||
"resourcePolicyAuthMethodsDescription": "Allow access to resources via additional auth methods",
|
||||
"resourcePolicyUsersRolesDescription": "Configure which users and roles can visit associated resources",
|
||||
"rulesResourcePolicyDescription": "Configure rules to control access resources associated to this policy",
|
||||
"authentication": "Authentication",
|
||||
"protected": "Protected",
|
||||
"notProtected": "Not Protected",
|
||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||
"resourcePolicyMessageRemove": "Once removed, the resource policy will no longer be accessible. All resources associated with the resource will be unlinked and left without authentication.",
|
||||
"resourcePolicyQuestionRemove": "Are you sure you want to remove the resource policy from the organization?",
|
||||
"resourceHTTP": "HTTPS Resource",
|
||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
@@ -249,6 +271,8 @@
|
||||
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
||||
"resourceBack": "Back to Resources",
|
||||
"resourceGoTo": "Go to Resource",
|
||||
"resourcePolicyDelete": "Delete Resource Policy",
|
||||
"resourcePolicyDeleteConfirm": "Confirm Delete Resource Policy",
|
||||
"resourceDelete": "Delete Resource",
|
||||
"resourceDeleteConfirm": "Confirm Delete Resource",
|
||||
"visibility": "Visibility",
|
||||
@@ -261,6 +285,8 @@
|
||||
"rules": "Rules",
|
||||
"resourceSettingDescription": "Configure the settings on the resource",
|
||||
"resourceSetting": "{resourceName} Settings",
|
||||
"resourcePolicySettingDescription": "Configure the settings on the resource policy",
|
||||
"resourcePolicySetting": "{policyName} Settings",
|
||||
"alwaysAllow": "Bypass Auth",
|
||||
"alwaysDeny": "Block Access",
|
||||
"passToAuth": "Pass to Auth",
|
||||
@@ -731,6 +757,16 @@
|
||||
"rulesNoOne": "No rules. Add a rule using the form.",
|
||||
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
||||
"rulesSubmit": "Save Rules",
|
||||
"policyErrorCreate": "Error creating policy",
|
||||
"policyErrorCreateDescription": "An error occurred when creating the policy",
|
||||
"policyErrorCreateMessageDescription": "An unexpected error occurred",
|
||||
"policyErrorUpdate": "Error updating policy",
|
||||
"policyErrorUpdateDescription": "An error occurred when updating the policy",
|
||||
"policyErrorUpdateMessageDescription": "An unexpected error occurred",
|
||||
"policyCreatedSuccess": "Resource policy succesfully created",
|
||||
"policyUpdatedSuccess": "Resource policy succesfully updated",
|
||||
"authMethodsSave": "Save auth methods",
|
||||
"rulesSave": "Save Rules",
|
||||
"resourceErrorCreate": "Error creating resource",
|
||||
"resourceErrorCreateDescription": "An error occurred when creating the resource",
|
||||
"resourceErrorCreateMessage": "Error creating resource:",
|
||||
@@ -794,6 +830,16 @@
|
||||
"pincodeAdd": "Add PIN Code",
|
||||
"pincodeRemove": "Remove PIN Code",
|
||||
"resourceAuthMethods": "Authentication Methods",
|
||||
"resourcePolicyAuthMethodsEmpty": "No authentication method",
|
||||
"resourcePolicyOtpEmpty": "No one time password",
|
||||
"resourcePolicyReadOnly": "This policy is Read only",
|
||||
"resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it on this page.",
|
||||
"resourcePolicyTypeSave": "Save Resource type",
|
||||
"resourcePolicySelect": "Select resource policy",
|
||||
"resourcePolicySelectError": "Select a resource policy",
|
||||
"resourcePolicyNotFound": "Policy not found",
|
||||
"resourcePolicySearch": "Search policies",
|
||||
"resourcePolicyRulesEmpty": "No authentication rules",
|
||||
"resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods",
|
||||
"resourceAuthSettingsSave": "Saved successfully",
|
||||
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
|
||||
@@ -829,6 +875,12 @@
|
||||
"resourcePincodeSetupTitle": "Set Pincode",
|
||||
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
||||
"resourceRoleDescription": "Admins can always access this resource.",
|
||||
"resourcePolicySelectTitle": "Resource Access Policy",
|
||||
"resourcePolicySelectDescription": "Select the resource policy type for authentication",
|
||||
"resourcePolicyInline": "Inline Resource Policy",
|
||||
"resourcePolicyInlineDescription": "Access Policy scoped to only this resource",
|
||||
"resourcePolicyShared": "Shared Resource Policy",
|
||||
"resourcePolicySharedDescription": "Access Policy shared accross multiple resources",
|
||||
"resourceUsersRoles": "Access Controls",
|
||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||
@@ -1358,6 +1410,8 @@
|
||||
"sidebarResources": "Resources",
|
||||
"sidebarProxyResources": "Public",
|
||||
"sidebarClientResources": "Private",
|
||||
"sidebarPolicies": "Policies",
|
||||
"sidebarResourcePolicies": "Resources",
|
||||
"sidebarAccessControl": "Access Control",
|
||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||
"sidebarTeam": "Team",
|
||||
@@ -2660,19 +2714,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Reason",
|
||||
"requestLogs": "HTTP Request Logs",
|
||||
"requestLogs": "HTTPS Request Logs",
|
||||
"requestAnalytics": "Request Analytics",
|
||||
"host": "Host",
|
||||
"location": "Location",
|
||||
"actionLogs": "Admin Action Logs",
|
||||
"sidebarLogsRequest": "HTTP Request Logs",
|
||||
"sidebarLogsRequest": "HTTPS Request Logs",
|
||||
"sidebarLogsAccess": "Authentication Logs",
|
||||
"sidebarLogsAction": "Admin Action Logs",
|
||||
"logRetention": "Log Retention",
|
||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
||||
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||
"logRetentionRequestLabel": "HTTP Request Log Retention",
|
||||
"logRetentionRequestLabel": "HTTPS Request Log Retention",
|
||||
"logRetentionRequestDescription": "How long to retain request logs",
|
||||
"logRetentionAccessLabel": "Authentication Log Retention",
|
||||
"logRetentionAccessDescription": "How long to retain access logs",
|
||||
@@ -3134,7 +3188,7 @@
|
||||
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||
"httpDestConnectionLogsTitle": "Network Logs",
|
||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||
"httpDestRequestLogsTitle": "HTTP Request Logs",
|
||||
"httpDestRequestLogsTitle": "HTTPS Request Logs",
|
||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||
"httpDestSaveChanges": "Save Changes",
|
||||
"httpDestCreateDestination": "Create Destination",
|
||||
@@ -3208,48 +3262,5 @@
|
||||
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
|
||||
"domainPickerWildcardCertWarningLink": "Learn more",
|
||||
"health": "Health",
|
||||
"domainPendingErrorTitle": "Verification Issue",
|
||||
"memberPortalTitle": "Resources",
|
||||
"memberPortalDescription": "Resources you have access to in this organization",
|
||||
"memberPortalSortBy": "Sort by...",
|
||||
"memberPortalSortNameAsc": "Name A-Z",
|
||||
"memberPortalSortNameDesc": "Name Z-A",
|
||||
"memberPortalSortDomainAsc": "Domain A-Z",
|
||||
"memberPortalSortDomainDesc": "Domain Z-A",
|
||||
"memberPortalSortEnabledFirst": "Enabled First",
|
||||
"memberPortalSortDisabledFirst": "Disabled First",
|
||||
"memberPortalRefresh": "Refresh",
|
||||
"memberPortalRefreshResources": "Refresh Resources",
|
||||
"memberPortalFailedToLoad": "Failed to load resources",
|
||||
"memberPortalFailedToLoadDescription": "Failed to load resources. Please check your connection and try again.",
|
||||
"memberPortalUnableToLoad": "Unable to Load Resources",
|
||||
"memberPortalTryAgain": "Try Again",
|
||||
"memberPortalNoResourcesFound": "No Resources Found",
|
||||
"memberPortalNoResourcesAvailable": "No Resources Available",
|
||||
"memberPortalNoResourcesMatchSearch": "No resources match \"{query}\". Try adjusting your search terms or clearing the search to see all resources.",
|
||||
"memberPortalNoResourcesAccess": "You don't have access to any resources yet. Contact your administrator to get access to resources you need.",
|
||||
"memberPortalClearSearch": "Clear Search",
|
||||
"memberPortalPublicResources": "Public Resources",
|
||||
"memberPortalPublicResourcesDescription": "Web applications and services accessible via browser",
|
||||
"memberPortalCopiedToClipboard": "Copied to clipboard",
|
||||
"memberPortalCopiedUrlDescription": "Resource URL has been copied to your clipboard.",
|
||||
"memberPortalOpenResource": "Open Resource",
|
||||
"memberPortalPrivateResources": "Private Resources",
|
||||
"memberPortalPrivateResourcesDescription": "Internal network resources accessible via client",
|
||||
"memberPortalResourceDetails": "Resource Details",
|
||||
"memberPortalMode": "Mode",
|
||||
"memberPortalDestination": "Destination",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "Resource alias has been copied to your clipboard.",
|
||||
"memberPortalCopiedDestinationDescription": "Resource destination has been copied to your clipboard.",
|
||||
"memberPortalRequiresClientConnection": "Requires Client Connection",
|
||||
"memberPortalAuthMethods": "Authentication Methods",
|
||||
"memberPortalSso": "Single Sign-On (SSO)",
|
||||
"memberPortalPasswordProtected": "Password Protected",
|
||||
"memberPortalPinCode": "PIN Code",
|
||||
"memberPortalEmailWhitelist": "Email Whitelist",
|
||||
"memberPortalResourceDisabled": "Resource Disabled",
|
||||
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
|
||||
"memberPortalPrevious": "Previous",
|
||||
"memberPortalNext": "Next"
|
||||
"domainPendingErrorTitle": "Verification Issue"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Razón",
|
||||
"requestLogs": "Registros de Solicitud HTTP",
|
||||
"requestLogs": "Registros de Solicitud",
|
||||
"requestAnalytics": "Analítica de Solicitud",
|
||||
"host": "Anfitrión",
|
||||
"location": "Ubicación",
|
||||
"actionLogs": "Registros de acción",
|
||||
"sidebarLogsRequest": "Registros de Solicitud HTTP",
|
||||
"sidebarLogsRequest": "Registros de Solicitud",
|
||||
"sidebarLogsAccess": "Registros de acceso",
|
||||
"sidebarLogsAction": "Registros de acción",
|
||||
"logRetention": "Retención de Log",
|
||||
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
||||
"requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización",
|
||||
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
||||
"logRetentionRequestLabel": "Retención de Registro de Solicitud HTTP",
|
||||
"logRetentionRequestLabel": "Retención de Registro de Solicitud",
|
||||
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
||||
"logRetentionAccessLabel": "Retención de Log de Acceso",
|
||||
"logRetentionAccessDescription": "Cuánto tiempo retener los registros de acceso",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Acciones administrativas realizadas por los usuarios dentro de la organización.",
|
||||
"httpDestConnectionLogsTitle": "Registros de conexión",
|
||||
"httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.",
|
||||
"httpDestRequestLogsTitle": "Registros de Solicitud HTTP",
|
||||
"httpDestRequestLogsTitle": "Registros de Solicitud",
|
||||
"httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.",
|
||||
"httpDestSaveChanges": "Guardar Cambios",
|
||||
"httpDestCreateDestination": "Crear destino",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
|
||||
"domainPickerWildcardCertWarningLink": "Más información",
|
||||
"health": "Salud",
|
||||
"domainPendingErrorTitle": "Problema de verificación",
|
||||
"memberPortalTitle": "Recursos",
|
||||
"memberPortalDescription": "Recursos a los que tiene acceso en esta organización",
|
||||
"memberPortalSortBy": "Ordenar por...",
|
||||
"memberPortalSortNameAsc": "Nombre A-Z",
|
||||
"memberPortalSortNameDesc": "Nombre Z-A",
|
||||
"memberPortalSortDomainAsc": "Dominio A-Z",
|
||||
"memberPortalSortDomainDesc": "Dominio Z-A",
|
||||
"memberPortalSortEnabledFirst": "Habilitado Primero",
|
||||
"memberPortalSortDisabledFirst": "Deshabilitado Primero",
|
||||
"memberPortalRefresh": "Actualizar",
|
||||
"memberPortalRefreshResources": "Actualizar Recursos",
|
||||
"memberPortalFailedToLoad": "No se pudieron cargar los recursos",
|
||||
"memberPortalFailedToLoadDescription": "No se pudieron cargar los recursos. Por favor, revise su conexión e intente de nuevo.",
|
||||
"memberPortalUnableToLoad": "No se pudieron cargar los recursos",
|
||||
"memberPortalTryAgain": "Intentar de Nuevo",
|
||||
"memberPortalNoResourcesFound": "No se encontraron Recursos",
|
||||
"memberPortalNoResourcesAvailable": "No Hay Recursos Disponibles",
|
||||
"memberPortalNoResourcesMatchSearch": "No hay recursos que coincidan con \"{query}\". Intenta ajustar tus términos de búsqueda o limpiar la búsqueda para ver todos los recursos.",
|
||||
"memberPortalNoResourcesAccess": "Aún no tiene acceso a ningún recurso. Comuníquese con su administrador para obtener acceso a los recursos que necesita.",
|
||||
"memberPortalClearSearch": "Limpiar Búsqueda",
|
||||
"memberPortalPublicResources": "Recursos Públicos",
|
||||
"memberPortalPublicResourcesDescription": "Aplicaciones web y servicios accesibles vía navegador",
|
||||
"memberPortalCopiedToClipboard": "Copiado al portapapeles",
|
||||
"memberPortalCopiedUrlDescription": "La URL del recurso ha sido copiada a su portapapeles.",
|
||||
"memberPortalOpenResource": "Abrir Recurso",
|
||||
"memberPortalPrivateResources": "Recursos Privados",
|
||||
"memberPortalPrivateResourcesDescription": "Recursos de red interna accesibles vía cliente",
|
||||
"memberPortalResourceDetails": "Detalles del Recurso",
|
||||
"memberPortalMode": "Modo",
|
||||
"memberPortalDestination": "Destino",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "El alias del recurso ha sido copiado a su portapapeles.",
|
||||
"memberPortalCopiedDestinationDescription": "El destino del recurso ha sido copiado a su portapapeles.",
|
||||
"memberPortalRequiresClientConnection": "Requiere Conexión de Cliente",
|
||||
"memberPortalAuthMethods": "Métodos de Autenticación",
|
||||
"memberPortalSso": "Inicio de Sesión Único (SSO)",
|
||||
"memberPortalPasswordProtected": "Protegido por Contraseña",
|
||||
"memberPortalPinCode": "Código PIN",
|
||||
"memberPortalEmailWhitelist": "Lista Blanca de Correo",
|
||||
"memberPortalResourceDisabled": "Recurso Deshabilitado",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
||||
"memberPortalPrevious": "Anterior",
|
||||
"memberPortalNext": "Siguiente"
|
||||
"domainPendingErrorTitle": "Problema de verificación"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Raison",
|
||||
"requestLogs": "Journal des Requêtes HTTP",
|
||||
"requestLogs": "Journal des requêtes",
|
||||
"requestAnalytics": "Demander des analyses",
|
||||
"host": "Hôte",
|
||||
"location": "Localisation",
|
||||
"actionLogs": "Journaux des actions",
|
||||
"sidebarLogsRequest": "Journal des Requêtes HTTP",
|
||||
"sidebarLogsRequest": "Journal des requêtes",
|
||||
"sidebarLogsAccess": "Journaux d'accès",
|
||||
"sidebarLogsAction": "Journaux des actions",
|
||||
"logRetention": "Journaliser la rétention",
|
||||
"logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver",
|
||||
"requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation",
|
||||
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
||||
"logRetentionRequestLabel": "Rétention des Journaux de Requêtes HTTP",
|
||||
"logRetentionRequestLabel": "Demander la rétention des journaux",
|
||||
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
||||
"logRetentionAccessLabel": "Rétention du journal d'accès",
|
||||
"logRetentionAccessDescription": "Durée de conservation des journaux d'accès",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Actions administratives effectuées par les utilisateurs au sein de l'organisation.",
|
||||
"httpDestConnectionLogsTitle": "Journaux de connexion",
|
||||
"httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.",
|
||||
"httpDestRequestLogsTitle": "Journal des Requêtes HTTP",
|
||||
"httpDestRequestLogsTitle": "Journal des requêtes",
|
||||
"httpDestRequestLogsDescription": "Journaux des requêtes HTTP pour les ressources proxiées, y compris la méthode, le chemin et le code de réponse.",
|
||||
"httpDestSaveChanges": "Enregistrer les modifications",
|
||||
"httpDestCreateDestination": "Créer une destination",
|
||||
@@ -3209,48 +3209,5 @@
|
||||
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
|
||||
"domainPickerWildcardCertWarningLink": "En savoir plus",
|
||||
"health": "Santé",
|
||||
"domainPendingErrorTitle": "Problème de vérification",
|
||||
"memberPortalTitle": "Ressources",
|
||||
"memberPortalDescription": "Ressources auxquelles vous avez accès dans cette organisation",
|
||||
"memberPortalSortBy": "Trier par...",
|
||||
"memberPortalSortNameAsc": "Nom A-Z",
|
||||
"memberPortalSortNameDesc": "Nom Z-A",
|
||||
"memberPortalSortDomainAsc": "Domaine A-Z",
|
||||
"memberPortalSortDomainDesc": "Domaine Z-A",
|
||||
"memberPortalSortEnabledFirst": "Activé en premier",
|
||||
"memberPortalSortDisabledFirst": "Désactivé en premier",
|
||||
"memberPortalRefresh": "Actualiser",
|
||||
"memberPortalRefreshResources": "Actualiser les ressources",
|
||||
"memberPortalFailedToLoad": "Échec du chargement des ressources",
|
||||
"memberPortalFailedToLoadDescription": "Échec du chargement des ressources. Veuillez vérifier votre connexion et réessayer.",
|
||||
"memberPortalUnableToLoad": "Impossible de charger les ressources",
|
||||
"memberPortalTryAgain": "Réessayer",
|
||||
"memberPortalNoResourcesFound": "Aucune ressource trouvée",
|
||||
"memberPortalNoResourcesAvailable": "Aucune ressource disponible",
|
||||
"memberPortalNoResourcesMatchSearch": "Aucune ressource ne correspond à \"{query}\". Essayez d'ajuster vos termes de recherche ou de vider la recherche pour voir toutes les ressources.",
|
||||
"memberPortalNoResourcesAccess": "Vous n'avez encore accès à aucune ressource. Contactez votre administrateur pour obtenir l'accès aux ressources dont vous avez besoin.",
|
||||
"memberPortalClearSearch": "Effacer la recherche",
|
||||
"memberPortalPublicResources": "Ressources publiques",
|
||||
"memberPortalPublicResourcesDescription": "Applications et services web accessibles via un navigateur",
|
||||
"memberPortalCopiedToClipboard": "Copié dans le presse-papiers",
|
||||
"memberPortalCopiedUrlDescription": "L'URL de la ressource a été copiée dans votre presse-papiers.",
|
||||
"memberPortalOpenResource": "Ouvrir la ressource",
|
||||
"memberPortalPrivateResources": "Ressources privées",
|
||||
"memberPortalPrivateResourcesDescription": "Ressources réseau internes accessibles via un client",
|
||||
"memberPortalResourceDetails": "Détails de la ressource",
|
||||
"memberPortalMode": "Mode",
|
||||
"memberPortalDestination": "Destination",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "L'alias de la ressource a été copié dans votre presse-papiers.",
|
||||
"memberPortalCopiedDestinationDescription": "La destination de la ressource a été copiée dans votre presse-papiers.",
|
||||
"memberPortalRequiresClientConnection": "Nécessite une connexion client",
|
||||
"memberPortalAuthMethods": "Méthodes d'authentification",
|
||||
"memberPortalSso": "Authentification unique (SSO)",
|
||||
"memberPortalPasswordProtected": "Protégé par un mot de passe",
|
||||
"memberPortalPinCode": "Code PIN",
|
||||
"memberPortalEmailWhitelist": "Liste blanche des e-mails",
|
||||
"memberPortalResourceDisabled": "Ressource désactivée",
|
||||
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
|
||||
"memberPortalPrevious": "Précédent",
|
||||
"memberPortalNext": "Suivant"
|
||||
"domainPendingErrorTitle": "Problème de vérification"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Motivo",
|
||||
"requestLogs": "Log Richieste HTTP",
|
||||
"requestLogs": "Log Richiesta",
|
||||
"requestAnalytics": "Richiedi Analisi",
|
||||
"host": "Host",
|
||||
"location": "Posizione",
|
||||
"actionLogs": "Log Azioni",
|
||||
"sidebarLogsRequest": "Log Richieste HTTP",
|
||||
"sidebarLogsRequest": "Log Richiesta",
|
||||
"sidebarLogsAccess": "Log Accesso",
|
||||
"sidebarLogsAction": "Log Azioni",
|
||||
"logRetention": "Ritenzione Registro",
|
||||
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
||||
"requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione",
|
||||
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
||||
"logRetentionRequestLabel": "Conservazione Log Richieste HTTP",
|
||||
"logRetentionRequestLabel": "Richiedi Ritenzione Log",
|
||||
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
||||
"logRetentionAccessLabel": "Ritenzione Registro Accesso",
|
||||
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.",
|
||||
"httpDestConnectionLogsTitle": "Log Di Connessione",
|
||||
"httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.",
|
||||
"httpDestRequestLogsTitle": "Log Richieste HTTP",
|
||||
"httpDestRequestLogsTitle": "Log Richiesta",
|
||||
"httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.",
|
||||
"httpDestSaveChanges": "Salva Modifiche",
|
||||
"httpDestCreateDestination": "Crea Destinazione",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
|
||||
"domainPickerWildcardCertWarningLink": "Scopri di più",
|
||||
"health": "Salute",
|
||||
"domainPendingErrorTitle": "Problema di Verifica",
|
||||
"memberPortalTitle": "Risorse",
|
||||
"memberPortalDescription": "Risorse a cui hai accesso in questa organizzazione",
|
||||
"memberPortalSortBy": "Ordina per...",
|
||||
"memberPortalSortNameAsc": "Nome A-Z",
|
||||
"memberPortalSortNameDesc": "Nome Z-A",
|
||||
"memberPortalSortDomainAsc": "Dominio A-Z",
|
||||
"memberPortalSortDomainDesc": "Dominio Z-A",
|
||||
"memberPortalSortEnabledFirst": "Abilitati per primi",
|
||||
"memberPortalSortDisabledFirst": "Disabilitati per primi",
|
||||
"memberPortalRefresh": "Aggiorna",
|
||||
"memberPortalRefreshResources": "Aggiorna Risorse",
|
||||
"memberPortalFailedToLoad": "Caricamento delle risorse non riuscito",
|
||||
"memberPortalFailedToLoadDescription": "Caricamento delle risorse non riuscito. Controlla la tua connessione e riprova.",
|
||||
"memberPortalUnableToLoad": "Impossibile caricare le risorse",
|
||||
"memberPortalTryAgain": "Riprova",
|
||||
"memberPortalNoResourcesFound": "Nessuna risorsa trovata",
|
||||
"memberPortalNoResourcesAvailable": "Nessuna risorsa disponibile",
|
||||
"memberPortalNoResourcesMatchSearch": "Nessuna risorsa corrisponde a \"{query}\". Prova ad aggiustare i termini di ricerca o a cancellare la ricerca per vedere tutte le risorse.",
|
||||
"memberPortalNoResourcesAccess": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per ottenere l'accesso alle risorse di cui hai bisogno.",
|
||||
"memberPortalClearSearch": "Cancella Ricerca",
|
||||
"memberPortalPublicResources": "Risorse Pubbliche",
|
||||
"memberPortalPublicResourcesDescription": "Applicazioni web e servizi accessibili tramite browser",
|
||||
"memberPortalCopiedToClipboard": "Copiato negli appunti",
|
||||
"memberPortalCopiedUrlDescription": "L'URL della risorsa è stato copiato negli appunti.",
|
||||
"memberPortalOpenResource": "Apri Risorsa",
|
||||
"memberPortalPrivateResources": "Risorse Private",
|
||||
"memberPortalPrivateResourcesDescription": "Risorse di rete interne accessibili tramite client",
|
||||
"memberPortalResourceDetails": "Dettagli della Risorsa",
|
||||
"memberPortalMode": "Modalità",
|
||||
"memberPortalDestination": "Destinazione",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "L'alias della risorsa è stato copiato negli appunti.",
|
||||
"memberPortalCopiedDestinationDescription": "La destinazione della risorsa è stata copiata negli appunti.",
|
||||
"memberPortalRequiresClientConnection": "Richiede Connessione Client",
|
||||
"memberPortalAuthMethods": "Metodi di Autenticazione",
|
||||
"memberPortalSso": "Accesso unico (Single Sign-On, SSO)",
|
||||
"memberPortalPasswordProtected": "Protetto da password",
|
||||
"memberPortalPinCode": "Codice PIN",
|
||||
"memberPortalEmailWhitelist": "Lista Autorizzazioni Email",
|
||||
"memberPortalResourceDisabled": "Risorsa Disabilitata",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
|
||||
"memberPortalPrevious": "Precedente",
|
||||
"memberPortalNext": "Successivo"
|
||||
"domainPendingErrorTitle": "Problema di Verifica"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "유효한 인증 없음",
|
||||
"ip": "IP",
|
||||
"reason": "이유",
|
||||
"requestLogs": "HTTP 요청 로그",
|
||||
"requestLogs": "요청 로그",
|
||||
"requestAnalytics": "요청 분석",
|
||||
"host": "호스트",
|
||||
"location": "위치",
|
||||
"actionLogs": "작업 로그",
|
||||
"sidebarLogsRequest": "HTTP 요청 로그",
|
||||
"sidebarLogsRequest": "요청 로그",
|
||||
"sidebarLogsAccess": "접근 로그",
|
||||
"sidebarLogsAction": "작업 로그",
|
||||
"logRetention": "로그 보관",
|
||||
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
||||
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
||||
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
||||
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
|
||||
"logRetentionRequestLabel": "요청 로그 보관",
|
||||
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
||||
"logRetentionAccessLabel": "접근 로그 보관",
|
||||
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
||||
"httpDestConnectionLogsTitle": "연결 로그",
|
||||
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
||||
"httpDestRequestLogsTitle": "HTTP 요청 로그",
|
||||
"httpDestRequestLogsTitle": "요청 로그",
|
||||
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
||||
"httpDestSaveChanges": "변경 사항 저장",
|
||||
"httpDestCreateDestination": "대상지 생성",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
|
||||
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
|
||||
"health": "건강",
|
||||
"domainPendingErrorTitle": "확인 문제",
|
||||
"memberPortalTitle": "리소스",
|
||||
"memberPortalDescription": "이 조직에서 접근할 수 있는 리소스",
|
||||
"memberPortalSortBy": "정렬 기준...",
|
||||
"memberPortalSortNameAsc": "이름 A-Z",
|
||||
"memberPortalSortNameDesc": "이름 Z-A",
|
||||
"memberPortalSortDomainAsc": "도메인 A-Z",
|
||||
"memberPortalSortDomainDesc": "도메인 Z-A",
|
||||
"memberPortalSortEnabledFirst": "사용 활성화 우선",
|
||||
"memberPortalSortDisabledFirst": "사용 비활성화 우선",
|
||||
"memberPortalRefresh": "새로 고침",
|
||||
"memberPortalRefreshResources": "리소스 새로 고침",
|
||||
"memberPortalFailedToLoad": "리소스를 불러오는 데 실패했습니다",
|
||||
"memberPortalFailedToLoadDescription": "리소스를 불러오는 데 실패했습니다. 연결을 확인하고 다시 시도해 주십시오.",
|
||||
"memberPortalUnableToLoad": "리소스를 가져오는 데 실패했습니다",
|
||||
"memberPortalTryAgain": "다시 시도",
|
||||
"memberPortalNoResourcesFound": "리소스를 발견하지 못했습니다",
|
||||
"memberPortalNoResourcesAvailable": "사용 가능한 리소스가 없습니다",
|
||||
"memberPortalNoResourcesMatchSearch": "\"{query}\"와 일치하는 리소스가 없습니다. 검색어를 수정하거나 검색을 초기화하여 모든 리소스를 확인하십시오.",
|
||||
"memberPortalNoResourcesAccess": "아직 접근할 수 있는 리소스가 없습니다. 필요한 리소스 접근을 위해 관리자에게 문의하세요.",
|
||||
"memberPortalClearSearch": "검색 초기화",
|
||||
"memberPortalPublicResources": "공공 리소스",
|
||||
"memberPortalPublicResourcesDescription": "브라우저를 통해 접근 가능한 웹 애플리케이션 및 서비스",
|
||||
"memberPortalCopiedToClipboard": "클립보드에 복사됨",
|
||||
"memberPortalCopiedUrlDescription": "리소스 URL이 클립보드에 복사되었습니다.",
|
||||
"memberPortalOpenResource": "리소스 열기",
|
||||
"memberPortalPrivateResources": "비공개 리소스",
|
||||
"memberPortalPrivateResourcesDescription": "클라이언트를 통해 접근 가능한 내부 네트워크 리소스",
|
||||
"memberPortalResourceDetails": "리소스 세부 정보",
|
||||
"memberPortalMode": "모드",
|
||||
"memberPortalDestination": "대상지",
|
||||
"memberPortalAlias": "별칭",
|
||||
"memberPortalCopiedAliasDescription": "리소스 별칭이 클립보드에 복사되었습니다.",
|
||||
"memberPortalCopiedDestinationDescription": "리소스 대상지가 클립보드에 복사되었습니다.",
|
||||
"memberPortalRequiresClientConnection": "클라이언트 연결 필요",
|
||||
"memberPortalAuthMethods": "인증 방법",
|
||||
"memberPortalSso": "싱글 사인온 (SSO)",
|
||||
"memberPortalPasswordProtected": "비밀번호 보호",
|
||||
"memberPortalPinCode": "PIN 코드",
|
||||
"memberPortalEmailWhitelist": "이메일 화이트리스트",
|
||||
"memberPortalResourceDisabled": "리소스 비활성화됨",
|
||||
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
|
||||
"memberPortalPrevious": "이전",
|
||||
"memberPortalNext": "다음"
|
||||
"domainPendingErrorTitle": "확인 문제"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Grunn",
|
||||
"requestLogs": "HTTP-forespørselslogger",
|
||||
"requestLogs": "Forespørselslogger (Automatic Translation)",
|
||||
"requestAnalytics": "Be om analyser",
|
||||
"host": "Vert",
|
||||
"location": "Sted",
|
||||
"actionLogs": "Handlingslogger",
|
||||
"sidebarLogsRequest": "HTTP-forespørselslogger",
|
||||
"sidebarLogsRequest": "Forespørselslogger (Automatic Translation)",
|
||||
"sidebarLogsAccess": "Tilgangslogger (Automatic Translation)",
|
||||
"sidebarLogsAction": "Handlingslogger",
|
||||
"logRetention": "Logg tilbaketrekning",
|
||||
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
||||
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
||||
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
||||
"logRetentionRequestLabel": "Be om loggbevaring",
|
||||
"logRetentionRequestLabel": "Be om loggoverføring",
|
||||
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
||||
"logRetentionAccessLabel": "Få tilgang til loggoverføring",
|
||||
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
|
||||
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
|
||||
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
|
||||
"httpDestRequestLogsTitle": "HTTP-forespørselslogger",
|
||||
"httpDestRequestLogsTitle": "Forespørselslogger (Automatic Translation)",
|
||||
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
|
||||
"httpDestSaveChanges": "Lagre endringer",
|
||||
"httpDestCreateDestination": "Opprett mål",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
|
||||
"domainPickerWildcardCertWarningLink": "Lær mer",
|
||||
"health": "Helse",
|
||||
"domainPendingErrorTitle": "Verifiseringsproblem",
|
||||
"memberPortalTitle": "Ressurser",
|
||||
"memberPortalDescription": "Ressurser du har tilgang til i denne organisasjonen",
|
||||
"memberPortalSortBy": "Sorter etter...",
|
||||
"memberPortalSortNameAsc": "Navn A-Å",
|
||||
"memberPortalSortNameDesc": "Navn Å-A",
|
||||
"memberPortalSortDomainAsc": "Domene A-Å",
|
||||
"memberPortalSortDomainDesc": "Domene Å-A",
|
||||
"memberPortalSortEnabledFirst": "Aktivert først",
|
||||
"memberPortalSortDisabledFirst": "Deaktivert først",
|
||||
"memberPortalRefresh": "Oppdater",
|
||||
"memberPortalRefreshResources": "Oppdater ressurser",
|
||||
"memberPortalFailedToLoad": "Kunne ikke laste inn ressurser",
|
||||
"memberPortalFailedToLoadDescription": "Kunne ikke laste inn ressurser. Vennligst sjekk tilkoblingen din og prøv igjen.",
|
||||
"memberPortalUnableToLoad": "Kan ikke laste inn ressurser",
|
||||
"memberPortalTryAgain": "Prøv igjen",
|
||||
"memberPortalNoResourcesFound": "Ingen ressurser funnet",
|
||||
"memberPortalNoResourcesAvailable": "Ingen ressurser tilgjengelig",
|
||||
"memberPortalNoResourcesMatchSearch": "Ingen ressurser samsvarer med \"{query}\". Prøv å justere søkeordene dine eller fjern søket for å se alle ressurser.",
|
||||
"memberPortalNoResourcesAccess": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å få tilgang til de ressursene du trenger.",
|
||||
"memberPortalClearSearch": "Fjern søk",
|
||||
"memberPortalPublicResources": "Offentlige ressurser",
|
||||
"memberPortalPublicResourcesDescription": "Webapplikasjoner og -tjenester tilgjengelige via nettleser",
|
||||
"memberPortalCopiedToClipboard": "Kopiert til utklippstavlen",
|
||||
"memberPortalCopiedUrlDescription": "Ressurs-URL er kopiert til utklippstavlen din.",
|
||||
"memberPortalOpenResource": "Åpne ressurs",
|
||||
"memberPortalPrivateResources": "Private ressurser",
|
||||
"memberPortalPrivateResourcesDescription": "Interne nettverksressurser tilgjengelige via klient",
|
||||
"memberPortalResourceDetails": "Ressursdetaljer",
|
||||
"memberPortalMode": "Modus",
|
||||
"memberPortalDestination": "Destinasjon",
|
||||
"memberPortalAlias": "Navn",
|
||||
"memberPortalCopiedAliasDescription": "Ressursalias er kopiert til utklippstavlen din.",
|
||||
"memberPortalCopiedDestinationDescription": "Ressursdestinasjon er kopiert til utklippstavlen din.",
|
||||
"memberPortalRequiresClientConnection": "Krever klienttilkobling",
|
||||
"memberPortalAuthMethods": "Autentiseringsmetoder",
|
||||
"memberPortalSso": "Enkeltpålogging (SSO)",
|
||||
"memberPortalPasswordProtected": "Passordbeskyttet",
|
||||
"memberPortalPinCode": "PIN-kode",
|
||||
"memberPortalEmailWhitelist": "E-post-hviteliste",
|
||||
"memberPortalResourceDisabled": "Ressurs deaktivert",
|
||||
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
|
||||
"memberPortalPrevious": "Forrige",
|
||||
"memberPortalNext": "Neste"
|
||||
"domainPendingErrorTitle": "Verifiseringsproblem"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP-adres",
|
||||
"reason": "Reden",
|
||||
"requestLogs": "HTTP-aanvraaglogboeken",
|
||||
"requestLogs": "Logboeken aanvragen",
|
||||
"requestAnalytics": "Analytics opvragen",
|
||||
"host": "Hostnaam",
|
||||
"location": "Locatie",
|
||||
"actionLogs": "Actie logs",
|
||||
"sidebarLogsRequest": "HTTP-aanvraaglogboeken",
|
||||
"sidebarLogsRequest": "Logboeken aanvragen",
|
||||
"sidebarLogsAccess": "Toegang tot logboek",
|
||||
"sidebarLogsAction": "Actie logs",
|
||||
"logRetention": "Log bewaring",
|
||||
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
||||
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
||||
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
||||
"logRetentionRequestLabel": "Bewaring van HTTP-aanvraaglogboeken",
|
||||
"logRetentionRequestLabel": "Logboekbewaring aanvragen",
|
||||
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
||||
"logRetentionAccessLabel": "Toegang logboek bewaring",
|
||||
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
|
||||
"httpDestConnectionLogsTitle": "Connectie Logs",
|
||||
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
|
||||
"httpDestRequestLogsTitle": "HTTP-aanvraaglogboeken",
|
||||
"httpDestRequestLogsTitle": "Logboeken aanvragen",
|
||||
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
|
||||
"httpDestSaveChanges": "Wijzigingen opslaan",
|
||||
"httpDestCreateDestination": "Maak bestemming aan",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
|
||||
"domainPickerWildcardCertWarningLink": "Meer informatie",
|
||||
"health": "Gezondheid",
|
||||
"domainPendingErrorTitle": "Verificatieprobleem",
|
||||
"memberPortalTitle": "Bronnen",
|
||||
"memberPortalDescription": "Bronnen waartoe je toegang hebt binnen deze organisatie",
|
||||
"memberPortalSortBy": "Sorteren op...",
|
||||
"memberPortalSortNameAsc": "Naam A-Z",
|
||||
"memberPortalSortNameDesc": "Naam Z-A",
|
||||
"memberPortalSortDomainAsc": "Domein A-Z",
|
||||
"memberPortalSortDomainDesc": "Domein Z-A",
|
||||
"memberPortalSortEnabledFirst": "Ingeschakeld Eerst",
|
||||
"memberPortalSortDisabledFirst": "Uitgeschakeld Eerst",
|
||||
"memberPortalRefresh": "Vernieuwen",
|
||||
"memberPortalRefreshResources": "Bronnen Vernieuwen",
|
||||
"memberPortalFailedToLoad": "Fout bij het laden van bronnen",
|
||||
"memberPortalFailedToLoadDescription": "Fout bij het laden van bronnen. Controleer uw verbinding en probeer het opnieuw.",
|
||||
"memberPortalUnableToLoad": "Niet in staat om bronnen te laden",
|
||||
"memberPortalTryAgain": "Probeer Opnieuw",
|
||||
"memberPortalNoResourcesFound": "Geen Bronnen Gevonden",
|
||||
"memberPortalNoResourcesAvailable": "Geen Bronnen Beschikbaar",
|
||||
"memberPortalNoResourcesMatchSearch": "Geen bronnen komen overeen met \"{query}\". Probeer uw zoektermen aan te passen of wis de zoekopdracht om alle bronnen te zien.",
|
||||
"memberPortalNoResourcesAccess": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang te krijgen tot de benodigde bronnen.",
|
||||
"memberPortalClearSearch": "Zoekopdracht Wissen",
|
||||
"memberPortalPublicResources": "Publieke Bronnen",
|
||||
"memberPortalPublicResourcesDescription": "Webapplicaties en services toegankelijk via browser",
|
||||
"memberPortalCopiedToClipboard": "Gekopieerd naar klembord",
|
||||
"memberPortalCopiedUrlDescription": "Bron URL is naar uw klembord gekopieerd.",
|
||||
"memberPortalOpenResource": "Bron Openen",
|
||||
"memberPortalPrivateResources": "Privé Bronnen",
|
||||
"memberPortalPrivateResourcesDescription": "Interne netwerkbronnen toegankelijk via client",
|
||||
"memberPortalResourceDetails": "Bron Details",
|
||||
"memberPortalMode": "Modus",
|
||||
"memberPortalDestination": "Bestemming",
|
||||
"memberPortalAlias": "Alias",
|
||||
"memberPortalCopiedAliasDescription": "Bron alias is naar uw klembord gekopieerd.",
|
||||
"memberPortalCopiedDestinationDescription": "Bron bestemming is naar uw klembord gekopieerd.",
|
||||
"memberPortalRequiresClientConnection": "Clientverbinding Vereist",
|
||||
"memberPortalAuthMethods": "Authenticatiemethoden",
|
||||
"memberPortalSso": "Single Sign-On (SSO)",
|
||||
"memberPortalPasswordProtected": "Wachtwoord Beveiligd",
|
||||
"memberPortalPinCode": "Pincode",
|
||||
"memberPortalEmailWhitelist": "E-mail whitelist",
|
||||
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
|
||||
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
|
||||
"memberPortalPrevious": "Vorige",
|
||||
"memberPortalNext": "Volgende"
|
||||
"domainPendingErrorTitle": "Verificatieprobleem"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Powód",
|
||||
"requestLogs": "Dzienniki żądań HTTP",
|
||||
"requestLogs": "Dzienniki żądań",
|
||||
"requestAnalytics": "Żądanie Analityki",
|
||||
"host": "Host",
|
||||
"location": "Lokalizacja",
|
||||
"actionLogs": "Dzienniki działań",
|
||||
"sidebarLogsRequest": "Dzienniki żądań HTTP",
|
||||
"sidebarLogsRequest": "Dzienniki żądań",
|
||||
"sidebarLogsAccess": "Logi dostępu",
|
||||
"sidebarLogsAction": "Dzienniki działań",
|
||||
"logRetention": "Zachowanie dziennika",
|
||||
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
||||
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
||||
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
||||
"logRetentionRequestLabel": "Przechowywanie dzienników żądań HTTP",
|
||||
"logRetentionRequestLabel": "Zachowanie dziennika żądań",
|
||||
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
||||
"logRetentionAccessLabel": "Zachowanie dziennika dostępu",
|
||||
"logRetentionAccessDescription": "Jak długo zachować dzienniki dostępu",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Działania administracyjne wykonywane przez użytkowników w organizacji.",
|
||||
"httpDestConnectionLogsTitle": "Dzienniki połączeń",
|
||||
"httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.",
|
||||
"httpDestRequestLogsTitle": "Dzienniki żądań HTTP",
|
||||
"httpDestRequestLogsTitle": "Dzienniki żądań",
|
||||
"httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.",
|
||||
"httpDestSaveChanges": "Zapisz zmiany",
|
||||
"httpDestCreateDestination": "Utwórz cel",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
||||
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
||||
"health": "Zdrowie",
|
||||
"domainPendingErrorTitle": "Problem z weryfikacją",
|
||||
"memberPortalTitle": "Zasoby",
|
||||
"memberPortalDescription": "Zasoby, do których masz dostęp w tej organizacji",
|
||||
"memberPortalSortBy": "Sortuj według...",
|
||||
"memberPortalSortNameAsc": "Nazwa A-Z",
|
||||
"memberPortalSortNameDesc": "Nazwa Z-A",
|
||||
"memberPortalSortDomainAsc": "Domena A-Z",
|
||||
"memberPortalSortDomainDesc": "Domena Z-A",
|
||||
"memberPortalSortEnabledFirst": "Włączone najpierw",
|
||||
"memberPortalSortDisabledFirst": "Wyłączone najpierw",
|
||||
"memberPortalRefresh": "Odśwież",
|
||||
"memberPortalRefreshResources": "Odśwież zasoby",
|
||||
"memberPortalFailedToLoad": "Nie udało się załadować zasobów",
|
||||
"memberPortalFailedToLoadDescription": "Nie udało się załadować zasobów. Sprawdź połączenie i spróbuj ponownie.",
|
||||
"memberPortalUnableToLoad": "Nie można załadować zasobów",
|
||||
"memberPortalTryAgain": "Spróbuj ponownie",
|
||||
"memberPortalNoResourcesFound": "Nie znaleziono zasobów",
|
||||
"memberPortalNoResourcesAvailable": "Brak dostępnych zasobów",
|
||||
"memberPortalNoResourcesMatchSearch": "Żadne zasoby nie pasują do „{query}”. Spróbuj dostosować swoje warunki wyszukiwania lub wyczyść wyszukiwanie, aby zobaczyć wszystkie zasoby.",
|
||||
"memberPortalNoResourcesAccess": "Nie masz jeszcze dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby uzyskać dostęp do potrzebnych zasobów.",
|
||||
"memberPortalClearSearch": "Wyczyść wyszukiwanie",
|
||||
"memberPortalPublicResources": "Publiczne zasoby",
|
||||
"memberPortalPublicResourcesDescription": "Aplikacje i usługi internetowe dostępne za pośrednictwem przeglądarki",
|
||||
"memberPortalCopiedToClipboard": "Skopiowano do schowka",
|
||||
"memberPortalCopiedUrlDescription": "URL zasobu został skopiowany do schowka.",
|
||||
"memberPortalOpenResource": "Otwórz zasób",
|
||||
"memberPortalPrivateResources": "Prywatne zasoby",
|
||||
"memberPortalPrivateResourcesDescription": "Zasoby sieci wewnętrznej dostępne za pośrednictwem klienta",
|
||||
"memberPortalResourceDetails": "Szczegóły zasobu",
|
||||
"memberPortalMode": "Tryb",
|
||||
"memberPortalDestination": "Miejsce docelowe",
|
||||
"memberPortalAlias": "Pseudonim",
|
||||
"memberPortalCopiedAliasDescription": "Alias zasobu został skopiowany do schowka.",
|
||||
"memberPortalCopiedDestinationDescription": "Miejsce docelowe zasobu zostało skopiowane do schowka.",
|
||||
"memberPortalRequiresClientConnection": "Wymaga połączenia z klientem",
|
||||
"memberPortalAuthMethods": "Metody uwierzytelniania",
|
||||
"memberPortalSso": "Jednorazowe logowanie (SSO)",
|
||||
"memberPortalPasswordProtected": "Chronione hasłem",
|
||||
"memberPortalPinCode": "Kod PIN",
|
||||
"memberPortalEmailWhitelist": "Biała lista e-mail",
|
||||
"memberPortalResourceDisabled": "Zasób wyłączony",
|
||||
"memberPortalShowingResources": "Wyświetlanie zasobów od {start} do {end} z {total}",
|
||||
"memberPortalPrevious": "Poprzedni",
|
||||
"memberPortalNext": "Następny"
|
||||
"domainPendingErrorTitle": "Problem z weryfikacją"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "PI",
|
||||
"reason": "Motivo",
|
||||
"requestLogs": "Registros de Pedidos HTTP",
|
||||
"requestLogs": "Registro de pedidos",
|
||||
"requestAnalytics": "Solicitar análise",
|
||||
"host": "Servidor",
|
||||
"location": "Local:",
|
||||
"actionLogs": "Logs de Ações",
|
||||
"sidebarLogsRequest": "Registros de Pedidos HTTP",
|
||||
"sidebarLogsRequest": "Registro de pedidos",
|
||||
"sidebarLogsAccess": "Logs de Acesso",
|
||||
"sidebarLogsAction": "Logs de Ações",
|
||||
"logRetention": "Retenção de Log",
|
||||
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
||||
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
||||
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
||||
"logRetentionRequestLabel": "Retenção de Registro de Pedido HTTP",
|
||||
"logRetentionRequestLabel": "Solicitar retenção de registro",
|
||||
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
||||
"logRetentionAccessLabel": "Retenção de Log de Acesso",
|
||||
"logRetentionAccessDescription": "Por quanto tempo manter os registros de acesso",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Ações administrativas realizadas por usuários dentro da organização.",
|
||||
"httpDestConnectionLogsTitle": "Logs da conexão",
|
||||
"httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.",
|
||||
"httpDestRequestLogsTitle": "Registros de Pedidos HTTP",
|
||||
"httpDestRequestLogsTitle": "Registro de pedidos",
|
||||
"httpDestRequestLogsDescription": "Logs de solicitação HTTP para recursos proxy incluindo o método, o caminho e o código de resposta.",
|
||||
"httpDestSaveChanges": "Salvar as alterações",
|
||||
"httpDestCreateDestination": "Criar destino",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
|
||||
"domainPickerWildcardCertWarningLink": "Saiba mais",
|
||||
"health": "Saúde",
|
||||
"domainPendingErrorTitle": "Problema de Verificação",
|
||||
"memberPortalTitle": "Recursos",
|
||||
"memberPortalDescription": "Recursos aos quais você tem acesso nesta organização",
|
||||
"memberPortalSortBy": "Ordenar por...",
|
||||
"memberPortalSortNameAsc": "Nome A-Z",
|
||||
"memberPortalSortNameDesc": "Nome Z-A",
|
||||
"memberPortalSortDomainAsc": "Domínio A-Z",
|
||||
"memberPortalSortDomainDesc": "Domínio Z-A",
|
||||
"memberPortalSortEnabledFirst": "Habilitados Primeiro",
|
||||
"memberPortalSortDisabledFirst": "Desabilitados Primeiro",
|
||||
"memberPortalRefresh": "Atualizar",
|
||||
"memberPortalRefreshResources": "Atualizar Recursos",
|
||||
"memberPortalFailedToLoad": "Falha ao carregar recursos",
|
||||
"memberPortalFailedToLoadDescription": "Falha ao carregar recursos. Por favor, verifique sua conexão e tente novamente.",
|
||||
"memberPortalUnableToLoad": "Incapaz de Carregar Recursos",
|
||||
"memberPortalTryAgain": "Tentar Novamente",
|
||||
"memberPortalNoResourcesFound": "Nenhum Recurso Encontrado",
|
||||
"memberPortalNoResourcesAvailable": "Nenhum Recurso Disponível",
|
||||
"memberPortalNoResourcesMatchSearch": "Nenhum recurso corresponde a \"{query}\". Tente ajustar seus termos de pesquisa ou limpe a pesquisa para ver todos os recursos.",
|
||||
"memberPortalNoResourcesAccess": "Você ainda não tem acesso a nenhum recurso. Entre em contato com seu administrador para obter acesso aos recursos que precisa.",
|
||||
"memberPortalClearSearch": "Limpar Pesquisa",
|
||||
"memberPortalPublicResources": "Recursos Públicos",
|
||||
"memberPortalPublicResourcesDescription": "Aplicações e serviços web acessíveis via navegador",
|
||||
"memberPortalCopiedToClipboard": "Copiado para a área de transferência",
|
||||
"memberPortalCopiedUrlDescription": "A URL do recurso foi copiada para sua área de transferência.",
|
||||
"memberPortalOpenResource": "Abrir Recurso",
|
||||
"memberPortalPrivateResources": "Recursos Privados",
|
||||
"memberPortalPrivateResourcesDescription": "Recursos da rede interna acessíveis via cliente",
|
||||
"memberPortalResourceDetails": "Detalhes do Recurso",
|
||||
"memberPortalMode": "Modo",
|
||||
"memberPortalDestination": "Destino",
|
||||
"memberPortalAlias": "Apelido",
|
||||
"memberPortalCopiedAliasDescription": "O apelido do recurso foi copiado para sua área de transferência.",
|
||||
"memberPortalCopiedDestinationDescription": "O destino do recurso foi copiado para sua área de transferência.",
|
||||
"memberPortalRequiresClientConnection": "Requer Conexão de Cliente",
|
||||
"memberPortalAuthMethods": "Métodos de Autenticação",
|
||||
"memberPortalSso": "Logon Único (SSO)",
|
||||
"memberPortalPasswordProtected": "Protegido por Senha",
|
||||
"memberPortalPinCode": "Código PIN",
|
||||
"memberPortalEmailWhitelist": "Lista de E-mails Permitidos",
|
||||
"memberPortalResourceDisabled": "Recurso Desativado",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
||||
"memberPortalPrevious": "Anterior",
|
||||
"memberPortalNext": "Próximo"
|
||||
"domainPendingErrorTitle": "Problema de Verificação"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Причина",
|
||||
"requestLogs": "HTTP Запросы Логи",
|
||||
"requestLogs": "Запросить журналы",
|
||||
"requestAnalytics": "Аналитика запроса",
|
||||
"host": "Хост",
|
||||
"location": "Местоположение",
|
||||
"actionLogs": "Журнал действий",
|
||||
"sidebarLogsRequest": "HTTP Запросы Логи",
|
||||
"sidebarLogsRequest": "Запросить журналы",
|
||||
"sidebarLogsAccess": "Журналы доступа",
|
||||
"sidebarLogsAction": "Журнал действий",
|
||||
"logRetention": "Сохранение журнала",
|
||||
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
||||
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
||||
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
||||
"logRetentionRequestLabel": "Сохранение HTTP Запросов Лога",
|
||||
"logRetentionRequestLabel": "Запросить сохранение журнала",
|
||||
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
||||
"logRetentionAccessLabel": "Хранение журнала доступа",
|
||||
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
||||
"httpDestConnectionLogsTitle": "Журнал подключений",
|
||||
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
||||
"httpDestRequestLogsTitle": "HTTP Запросы Логи",
|
||||
"httpDestRequestLogsTitle": "Запросить журналы",
|
||||
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
||||
"httpDestSaveChanges": "Сохранить изменения",
|
||||
"httpDestCreateDestination": "Создать адрес назначения",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
|
||||
"domainPickerWildcardCertWarningLink": "Узнать больше",
|
||||
"health": "Состояние",
|
||||
"domainPendingErrorTitle": "Проблема с подтверждением",
|
||||
"memberPortalTitle": "Ресурсы",
|
||||
"memberPortalDescription": "Ресурсы, к которым у вас есть доступ в этой организации",
|
||||
"memberPortalSortBy": "Сортировать по...",
|
||||
"memberPortalSortNameAsc": "Имя A-Я",
|
||||
"memberPortalSortNameDesc": "Имя Я-A",
|
||||
"memberPortalSortDomainAsc": "Домен A-Я",
|
||||
"memberPortalSortDomainDesc": "Домен Я-A",
|
||||
"memberPortalSortEnabledFirst": "Включённые сначала",
|
||||
"memberPortalSortDisabledFirst": "Отключённые сначала",
|
||||
"memberPortalRefresh": "Обновить",
|
||||
"memberPortalRefreshResources": "Обновить ресурсы",
|
||||
"memberPortalFailedToLoad": "Не удалось загрузить ресурсы",
|
||||
"memberPortalFailedToLoadDescription": "Не удалось загрузить ресурсы. Пожалуйста, проверьте подключение и попробуйте снова.",
|
||||
"memberPortalUnableToLoad": "Не удалось загрузить ресурсы",
|
||||
"memberPortalTryAgain": "Попробуйте снова",
|
||||
"memberPortalNoResourcesFound": "Ресурсы не найдены",
|
||||
"memberPortalNoResourcesAvailable": "Нет доступных ресурсов",
|
||||
"memberPortalNoResourcesMatchSearch": "Нет ресурсов, соответствующих \"{query}\". Попробуйте изменить условия поиска или очистить поиск, чтобы увидеть все ресурсы.",
|
||||
"memberPortalNoResourcesAccess": "У вас пока нет доступа к ресурсам. Свяжитесь с администратором, чтобы получить доступ к нужным вам ресурсам.",
|
||||
"memberPortalClearSearch": "Очистить поиск",
|
||||
"memberPortalPublicResources": "Публичные ресурсы",
|
||||
"memberPortalPublicResourcesDescription": "Веб-приложения и сервисы, доступные через браузер",
|
||||
"memberPortalCopiedToClipboard": "Скопировано в буфер обмена",
|
||||
"memberPortalCopiedUrlDescription": "URL ресурса был скопирован в ваш буфер обмена.",
|
||||
"memberPortalOpenResource": "Открыть ресурс",
|
||||
"memberPortalPrivateResources": "Приватные ресурсы",
|
||||
"memberPortalPrivateResourcesDescription": "Ресурсы внутренней сети, доступные через клиент",
|
||||
"memberPortalResourceDetails": "Детали ресурса",
|
||||
"memberPortalMode": "Режим",
|
||||
"memberPortalDestination": "Назначение",
|
||||
"memberPortalAlias": "Псевдоним",
|
||||
"memberPortalCopiedAliasDescription": "Псевдоним ресурса был скопирован в ваш буфер обмена.",
|
||||
"memberPortalCopiedDestinationDescription": "Назначение ресурса было скопировано в ваш буфер обмена.",
|
||||
"memberPortalRequiresClientConnection": "Требуется подключение клиента",
|
||||
"memberPortalAuthMethods": "Методы аутентификации",
|
||||
"memberPortalSso": "Единый вход (SSO)",
|
||||
"memberPortalPasswordProtected": "Защищено паролем",
|
||||
"memberPortalPinCode": "PIN-код",
|
||||
"memberPortalEmailWhitelist": "Белый список email",
|
||||
"memberPortalResourceDisabled": "Ресурс отключён",
|
||||
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
|
||||
"memberPortalPrevious": "Предыдущий",
|
||||
"memberPortalNext": "Следующий"
|
||||
"domainPendingErrorTitle": "Проблема с подтверждением"
|
||||
}
|
||||
|
||||
@@ -2660,19 +2660,19 @@
|
||||
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
||||
"ip": "IP",
|
||||
"reason": "Sebep",
|
||||
"requestLogs": "HTTP İstek Günlükleri",
|
||||
"requestLogs": "İstek Günlükleri",
|
||||
"requestAnalytics": "İstek Analizi",
|
||||
"host": "Sunucu",
|
||||
"location": "Konum",
|
||||
"actionLogs": "Eylem Günlükleri",
|
||||
"sidebarLogsRequest": "HTTP İstek Günlükleri",
|
||||
"sidebarLogsRequest": "İstek Günlükleri",
|
||||
"sidebarLogsAccess": "Erişim Günlükleri",
|
||||
"sidebarLogsAction": "Eylem Günlükleri",
|
||||
"logRetention": "Kayıt Saklama",
|
||||
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
||||
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
||||
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
||||
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
|
||||
"logRetentionRequestLabel": "İstek Günlüğü Saklama",
|
||||
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
||||
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
||||
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
|
||||
@@ -3134,7 +3134,7 @@
|
||||
"httpDestActionLogsDescription": "Kullanıcılar tarafından organizasyon içerisinde yapılan yönetici eylemleri.",
|
||||
"httpDestConnectionLogsTitle": "Bağlantı Kayıtları",
|
||||
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
|
||||
"httpDestRequestLogsTitle": "HTTP İstek Günlükleri",
|
||||
"httpDestRequestLogsTitle": "İstek Kayıtları",
|
||||
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
|
||||
"httpDestSaveChanges": "Değişiklikleri Kaydet",
|
||||
"httpDestCreateDestination": "Hedef Oluştur",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
|
||||
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
|
||||
"health": "Sağlık",
|
||||
"domainPendingErrorTitle": "Doğrulama Sorunu",
|
||||
"memberPortalTitle": "Kaynaklar",
|
||||
"memberPortalDescription": "Bu organizasyondaki erişiminiz olan kaynaklar",
|
||||
"memberPortalSortBy": "Şuna göre sırala...",
|
||||
"memberPortalSortNameAsc": "İsim A-Z",
|
||||
"memberPortalSortNameDesc": "İsim Z-A",
|
||||
"memberPortalSortDomainAsc": "Alan A-Z",
|
||||
"memberPortalSortDomainDesc": "Alan Z-A",
|
||||
"memberPortalSortEnabledFirst": "İlk Etkinleştirilenler",
|
||||
"memberPortalSortDisabledFirst": "İlk Devre Dışı Bırakılanlar",
|
||||
"memberPortalRefresh": "Yenile",
|
||||
"memberPortalRefreshResources": "Kaynakları Yenile",
|
||||
"memberPortalFailedToLoad": "Kaynaklar yüklenemedi",
|
||||
"memberPortalFailedToLoadDescription": "Kaynaklar yüklenemedi. Lütfen bağlantınızı kontrol edin ve tekrar deneyin.",
|
||||
"memberPortalUnableToLoad": "Kaynaklar Yüklenemiyor",
|
||||
"memberPortalTryAgain": "Tekrar Dene",
|
||||
"memberPortalNoResourcesFound": "Hiçbir Kaynak Bulunamadı",
|
||||
"memberPortalNoResourcesAvailable": "Uygun Kaynak Yok",
|
||||
"memberPortalNoResourcesMatchSearch": "Hiçbir kaynak \"{query}\" ile eşleşmiyor. Arama terimlerinizi değiştirerek veya tüm kaynakları görmek için aramayı temizleyerek deneyin.",
|
||||
"memberPortalNoResourcesAccess": "Henüz herhangi bir kaynağa erişiminiz yok. İhtiyacınız olan kaynaklara erişim sağlamak için yöneticinizle iletişime geçin.",
|
||||
"memberPortalClearSearch": "Aramayı Temizle",
|
||||
"memberPortalPublicResources": "Genel Kaynaklar",
|
||||
"memberPortalPublicResourcesDescription": "Tarayıcı üzerinden erişilebilen web uygulamaları ve hizmetler",
|
||||
"memberPortalCopiedToClipboard": "Panoya kopyalandı",
|
||||
"memberPortalCopiedUrlDescription": "Kaynak URL'si panonuza kopyalandı.",
|
||||
"memberPortalOpenResource": "Kaynağı Aç",
|
||||
"memberPortalPrivateResources": "Özel Kaynaklar",
|
||||
"memberPortalPrivateResourcesDescription": "İstemci üzerinden erişilebilen dahili ağ kaynakları",
|
||||
"memberPortalResourceDetails": "Kaynak Detayları",
|
||||
"memberPortalMode": "Mod",
|
||||
"memberPortalDestination": "Hedef",
|
||||
"memberPortalAlias": "Takma İsim",
|
||||
"memberPortalCopiedAliasDescription": "Kaynak takma adı panonuza kopyalandı.",
|
||||
"memberPortalCopiedDestinationDescription": "Kaynak hedefi panonuza kopyalandı.",
|
||||
"memberPortalRequiresClientConnection": "İstemci Bağlantısı Gerektirir",
|
||||
"memberPortalAuthMethods": "Kimlik Doğrulama Yöntemleri",
|
||||
"memberPortalSso": "Tek Oturum Açma (SSO)",
|
||||
"memberPortalPasswordProtected": "Parola ile Korunan",
|
||||
"memberPortalPinCode": "PIN Kodu",
|
||||
"memberPortalEmailWhitelist": "E-posta Beyaz Listesi",
|
||||
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
|
||||
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
|
||||
"memberPortalPrevious": "Önceki",
|
||||
"memberPortalNext": "Sonraki"
|
||||
"domainPendingErrorTitle": "Doğrulama Sorunu"
|
||||
}
|
||||
|
||||
@@ -2672,7 +2672,7 @@
|
||||
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
||||
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
||||
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
||||
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
||||
"logRetentionRequestLabel": "请求日志保留",
|
||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||
"logRetentionAccessLabel": "访问日志保留",
|
||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||
@@ -3208,48 +3208,5 @@
|
||||
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
|
||||
"domainPickerWildcardCertWarningLink": "了解更多",
|
||||
"health": "健康",
|
||||
"domainPendingErrorTitle": "验证问题",
|
||||
"memberPortalTitle": "资源",
|
||||
"memberPortalDescription": "您在此组织中可以访问的资源",
|
||||
"memberPortalSortBy": "排序依据……",
|
||||
"memberPortalSortNameAsc": "名称 A-Z",
|
||||
"memberPortalSortNameDesc": "名称 Z-A",
|
||||
"memberPortalSortDomainAsc": "域名 A-Z",
|
||||
"memberPortalSortDomainDesc": "域名 Z-A",
|
||||
"memberPortalSortEnabledFirst": "启用优先",
|
||||
"memberPortalSortDisabledFirst": "禁用优先",
|
||||
"memberPortalRefresh": "刷新",
|
||||
"memberPortalRefreshResources": "刷新资源",
|
||||
"memberPortalFailedToLoad": "加载资源失败",
|
||||
"memberPortalFailedToLoadDescription": "加载资源失败。请检查您的连接并再试一次。",
|
||||
"memberPortalUnableToLoad": "无法加载资源",
|
||||
"memberPortalTryAgain": "再试一次",
|
||||
"memberPortalNoResourcesFound": "找不到资源",
|
||||
"memberPortalNoResourcesAvailable": "无可用资源",
|
||||
"memberPortalNoResourcesMatchSearch": "没有与\"{query}\"匹配的资源。尝试调整您的搜索词或清除搜索以查看所有资源。",
|
||||
"memberPortalNoResourcesAccess": "您尚无访问任何资源的权限。请联系您的管理员获取所需资源的访问权限。",
|
||||
"memberPortalClearSearch": "清除搜索",
|
||||
"memberPortalPublicResources": "公共资源",
|
||||
"memberPortalPublicResourcesDescription": "通过浏览器可访问的网络应用和服务",
|
||||
"memberPortalCopiedToClipboard": "已复制到剪贴板",
|
||||
"memberPortalCopiedUrlDescription": "资源 URL 已复制到您的剪贴板。",
|
||||
"memberPortalOpenResource": "打开资源",
|
||||
"memberPortalPrivateResources": "私有资源",
|
||||
"memberPortalPrivateResourcesDescription": "通过客户端可访问的内部网络资源",
|
||||
"memberPortalResourceDetails": "资源详情",
|
||||
"memberPortalMode": "模式",
|
||||
"memberPortalDestination": "目标",
|
||||
"memberPortalAlias": "别名",
|
||||
"memberPortalCopiedAliasDescription": "资源别名已复制到您的剪贴板。",
|
||||
"memberPortalCopiedDestinationDescription": "资源目的地已复制到您的剪贴板。",
|
||||
"memberPortalRequiresClientConnection": "需要客户端连接",
|
||||
"memberPortalAuthMethods": "身份验证方法",
|
||||
"memberPortalSso": "单一登录 (SSO)",
|
||||
"memberPortalPasswordProtected": "密码保护",
|
||||
"memberPortalPinCode": "PIN 码",
|
||||
"memberPortalEmailWhitelist": "电子邮件白名单",
|
||||
"memberPortalResourceDisabled": "资源已禁用",
|
||||
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
|
||||
"memberPortalPrevious": "上一页",
|
||||
"memberPortalNext": "下一页"
|
||||
"domainPendingErrorTitle": "验证问题"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export enum ActionsEnum {
|
||||
createOrgUser = "createOrgUser",
|
||||
@@ -152,7 +153,21 @@ export enum ActionsEnum {
|
||||
createHealthCheck = "createHealthCheck",
|
||||
updateHealthCheck = "updateHealthCheck",
|
||||
deleteHealthCheck = "deleteHealthCheck",
|
||||
listHealthChecks = "listHealthChecks"
|
||||
listHealthChecks = "listHealthChecks",
|
||||
listResourcePolicies = "listResourcePolicies",
|
||||
getResourcePolicy = "getResourcePolicy",
|
||||
createResourcePolicy = "createResourcePolicy",
|
||||
updateResourcePolicy = "updateResourcePolicy",
|
||||
deleteResourcePolicy = "deleteResourcePolicy",
|
||||
listResourcePolicyRoles = "listResourcePolicyRoles",
|
||||
setResourcePolicyRoles = "setResourcePolicyRoles",
|
||||
listResourcePolicyUsers = "listResourcePolicyUsers",
|
||||
setResourcePolicyUsers = "setResourcePolicyUsers",
|
||||
setResourcePolicyPassword = "setResourcePolicyPassword",
|
||||
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
@@ -185,6 +200,23 @@ export async function checkUserActionPermission(
|
||||
}
|
||||
}
|
||||
|
||||
// If no direct permission, check role-based permission (any of user's roles)
|
||||
const roleActionPermission = await db
|
||||
.select()
|
||||
.from(roleActions)
|
||||
.where(
|
||||
and(
|
||||
eq(roleActions.actionId, actionId),
|
||||
inArray(roleActions.roleId, userOrgRoleIds),
|
||||
eq(roleActions.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (roleActionPermission.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the user has direct permission for the action in the current org
|
||||
const userActionPermission = await db
|
||||
.select()
|
||||
@@ -202,20 +234,7 @@ export async function checkUserActionPermission(
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no direct permission, check role-based permission (any of user's roles)
|
||||
const roleActionPermission = await db
|
||||
.select()
|
||||
.from(roleActions)
|
||||
.where(
|
||||
and(
|
||||
eq(roleActions.actionId, actionId),
|
||||
inArray(roleActions.roleId, userOrgRoleIds),
|
||||
eq(roleActions.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return roleActionPermission.length > 0;
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking user action permission:", error);
|
||||
throw createHttpError(
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { join } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { clients, db, resources, siteResources } from "@server/db";
|
||||
import {
|
||||
clients,
|
||||
db,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
siteResources
|
||||
} from "@server/db";
|
||||
import { randomInt } from "crypto";
|
||||
import { exitNodes, sites } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
@@ -107,6 +113,35 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUniqueResourcePolicyName(
|
||||
orgId: string
|
||||
): Promise<string> {
|
||||
let loops = 0;
|
||||
while (true) {
|
||||
if (loops > 100) {
|
||||
throw new Error("Could not generate a unique name");
|
||||
}
|
||||
|
||||
const name = generateName();
|
||||
const policyCount = await db
|
||||
.select({
|
||||
niceId: resourcePolicies.niceId,
|
||||
orgId: resourcePolicies.orgId
|
||||
})
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicies.niceId, name),
|
||||
eq(resourcePolicies.orgId, orgId)
|
||||
)
|
||||
);
|
||||
if (policyCount.length === 0) {
|
||||
return name;
|
||||
}
|
||||
loops++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUniqueSiteResourceName(
|
||||
orgId: string
|
||||
): Promise<string> {
|
||||
|
||||
@@ -110,6 +110,16 @@ export const sites = pgTable("sites", {
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
resourceId: serial("resourceId").primaryKey(),
|
||||
resourcePolicyId: integer("resourcePolicyId").references(
|
||||
() => resourcePolicies.resourcePolicyId,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
|
||||
() => resourcePolicies.resourcePolicyId,
|
||||
{
|
||||
onDelete: "restrict"
|
||||
}
|
||||
),
|
||||
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
@@ -196,9 +206,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}).notNull(),
|
||||
siteId: integer("siteId")
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name"),
|
||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||
hcPath: varchar("hcPath"),
|
||||
@@ -521,6 +533,38 @@ export const userResources = pgTable("userResources", {
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const rolePolicies = pgTable("rolePolicies", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const userPolicies = pgTable("userPolicies", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyWhiteList = pgTable("resourcePolicyWhitelist", {
|
||||
whitelistId: serial("id").primaryKey(),
|
||||
email: varchar("email").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const userInvites = pgTable("userInvites", {
|
||||
inviteId: varchar("inviteId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
@@ -586,6 +630,40 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||
}
|
||||
);
|
||||
|
||||
export const resourcePolicyPincode = pgTable("resourcePolicyPincode", {
|
||||
pincodeId: serial("pincodeId").primaryKey(),
|
||||
pincodeHash: varchar("pincodeHash").notNull(),
|
||||
digitLength: integer("digitLength").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyPassword = pgTable("resourcePolicyPassword", {
|
||||
passwordId: serial("passwordId").primaryKey(),
|
||||
passwordHash: varchar("passwordHash").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyHeaderAuth = pgTable("resourcePolicyHeaderAuth", {
|
||||
headerAuthId: serial("headerAuthId").primaryKey(),
|
||||
headerAuthHash: varchar("headerAuthHash").notNull(),
|
||||
extendedCompatibility: boolean("extendedCompatibility")
|
||||
.notNull()
|
||||
.default(true),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
@@ -679,6 +757,43 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
|
||||
export const resourcePolicyRules = pgTable("resourcePolicyRules", {
|
||||
ruleId: serial("ruleId").primaryKey(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
|
||||
match: varchar("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
|
||||
export const resourcePolicies = pgTable("resourcePolicies", {
|
||||
resourcePolicyId: serial("resourcePolicyId").primaryKey(),
|
||||
sso: boolean("sso").notNull().default(true),
|
||||
applyRules: boolean("applyRules").notNull().default(false),
|
||||
scope: varchar("scope")
|
||||
.$type<"global" | "resource">()
|
||||
.notNull()
|
||||
.default("global"),
|
||||
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
idpId: integer("idpId").references(() => idp.idpId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const supporterKey = pgTable("supporterKey", {
|
||||
keyId: serial("keyId").primaryKey(),
|
||||
key: varchar("key").notNull(),
|
||||
@@ -1097,19 +1212,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
||||
complete: boolean("complete").notNull().default(false)
|
||||
});
|
||||
|
||||
export const statusHistory = pgTable("statusHistory", {
|
||||
id: serial("id").primaryKey(),
|
||||
entityType: varchar("entityType").notNull(),
|
||||
entityId: integer("entityId").notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: varchar("status").notNull(),
|
||||
timestamp: integer("timestamp").notNull(),
|
||||
}, (table) => [
|
||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||
]);
|
||||
export const statusHistory = pgTable(
|
||||
"statusHistory",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
entityType: varchar("entityType").notNull(),
|
||||
entityId: integer("entityId").notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: varchar("status").notNull(),
|
||||
timestamp: integer("timestamp").notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_statusHistory_entity").on(
|
||||
table.entityType,
|
||||
table.entityId,
|
||||
table.timestamp
|
||||
),
|
||||
index("idx_statusHistory_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
@@ -1179,3 +1305,6 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
>;
|
||||
export type Network = InferSelectModel<typeof networks>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||
|
||||
@@ -17,10 +17,13 @@ import {
|
||||
resourceHeaderAuth,
|
||||
ResourceHeaderAuth,
|
||||
resourceRules,
|
||||
resourcePolicyRules,
|
||||
resources,
|
||||
roleResources,
|
||||
rolePolicies,
|
||||
sessions,
|
||||
userResources,
|
||||
userPolicies,
|
||||
users,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
resourceHeaderAuthExtendedCompatibility
|
||||
@@ -154,58 +157,126 @@ export async function getRoleName(roleId: number): Promise<string | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role has access to resource
|
||||
* Check if role has access to resource (direct or via resource policy)
|
||||
*/
|
||||
export async function getRoleResourceAccess(
|
||||
resourceId: number,
|
||||
roleIds: number[]
|
||||
) {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
const [direct, viaPolicies] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
),
|
||||
db
|
||||
.select({
|
||||
roleId: rolePolicies.roleId,
|
||||
resourcePolicyId: rolePolicies.resourcePolicyId
|
||||
})
|
||||
.from(rolePolicies)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resources.resourcePolicyId, rolePolicies.resourcePolicyId)
|
||||
)
|
||||
);
|
||||
.where(
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
inArray(rolePolicies.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||
const combined = [...direct, ...viaPolicies];
|
||||
return combined.length > 0 ? combined : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has direct access to resource
|
||||
* Check if user has access to resource (direct or via resource policy)
|
||||
*/
|
||||
export async function getUserResourceAccess(
|
||||
userId: string,
|
||||
resourceId: number
|
||||
) {
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
const [direct, viaPolicies] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
.limit(1),
|
||||
db
|
||||
.select({
|
||||
userId: userPolicies.userId,
|
||||
resourcePolicyId: userPolicies.resourcePolicyId
|
||||
})
|
||||
.from(userPolicies)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resources.resourcePolicyId, userPolicies.resourcePolicyId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(userPolicies.userId, userId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
]);
|
||||
|
||||
return userResourceAccess.length > 0 ? userResourceAccess[0] : null;
|
||||
return direct[0] ?? viaPolicies[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource rules for a given resource
|
||||
* Get resource rules for a given resource (direct and via resource policy)
|
||||
*/
|
||||
export async function getResourceRules(
|
||||
resourceId: number
|
||||
): Promise<ResourceRule[]> {
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
const [directRules, policyRules] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId)),
|
||||
db
|
||||
.select({
|
||||
ruleId: resourcePolicyRules.ruleId,
|
||||
resourceId: sql<number>`${resourceId}`,
|
||||
enabled: resourcePolicyRules.enabled,
|
||||
priority: resourcePolicyRules.priority,
|
||||
action: resourcePolicyRules.action,
|
||||
match: resourcePolicyRules.match,
|
||||
value: resourcePolicyRules.value
|
||||
})
|
||||
.from(resourcePolicyRules)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(
|
||||
resources.resourcePolicyId,
|
||||
resourcePolicyRules.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
]);
|
||||
|
||||
return rules;
|
||||
const maxDirectPriority = directRules.reduce(
|
||||
(max, r) => Math.max(max, r.priority),
|
||||
0
|
||||
);
|
||||
const offsetPolicyRules = policyRules.map((r) => ({
|
||||
...r,
|
||||
priority: maxDirectPriority + r.priority
|
||||
}));
|
||||
|
||||
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -121,6 +121,16 @@ export const sites = sqliteTable("sites", {
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||
resourcePolicyId: integer("resourcePolicyId").references(
|
||||
() => resourcePolicies.resourcePolicyId,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
|
||||
() => resourcePolicies.resourcePolicyId,
|
||||
{
|
||||
onDelete: "restrict"
|
||||
}
|
||||
),
|
||||
resourceGuid: text("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
@@ -219,9 +229,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}).notNull(),
|
||||
siteId: integer("siteId")
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: text("name"),
|
||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
@@ -909,6 +921,47 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
||||
headerAuthHash: text("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourcePolicyPincode = sqliteTable("resourcePolicyPincode", {
|
||||
pincodeId: integer("pincodeId").primaryKey({ autoIncrement: true }),
|
||||
pincodeHash: text("pincodeHash").notNull(),
|
||||
digitLength: integer("digitLength").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyPassword = sqliteTable("resourcePolicyPassword", {
|
||||
passwordId: integer("passwordId").primaryKey({ autoIncrement: true }),
|
||||
passwordHash: text("passwordHash").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyHeaderAuth = sqliteTable(
|
||||
"resourcePolicyHeaderAuth",
|
||||
{
|
||||
headerAuthId: integer("headerAuthId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
headerAuthHash: text("headerAuthHash").notNull(),
|
||||
extendedCompatibility: integer("extendedCompatibility", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(true),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||
"resourceHeaderAuthExtendedCompatibility",
|
||||
{
|
||||
@@ -1023,6 +1076,77 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
value: text("value").notNull()
|
||||
});
|
||||
|
||||
export const rolePolicies = sqliteTable("rolePolicies", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const userPolicies = sqliteTable("userPolicies", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyWhiteList = sqliteTable("resourcePolicyWhitelist", {
|
||||
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
email: text("email").notNull(),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const resourcePolicyRules = sqliteTable("resourcePolicyRules", {
|
||||
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||
resourcePolicyId: integer("resourcePolicyId")
|
||||
.notNull()
|
||||
.references(() => resourcePolicies.resourcePolicyId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
|
||||
match: text("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
|
||||
value: text("value").notNull()
|
||||
});
|
||||
|
||||
export const resourcePolicies = sqliteTable("resourcePolicies", {
|
||||
resourcePolicyId: integer("resourcePolicyId").primaryKey(),
|
||||
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
||||
applyRules: integer("applyRules", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
scope: text("scope")
|
||||
.$type<"global" | "resource">()
|
||||
.notNull()
|
||||
.default("global"),
|
||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
niceId: text("niceId").notNull(),
|
||||
idpId: integer("idpId").references(() => idp.idpId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const supporterKey = sqliteTable("supporterKey", {
|
||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||
key: text("key").notNull(),
|
||||
@@ -1196,19 +1320,30 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
||||
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const statusHistory = sqliteTable("statusHistory", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
||||
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
||||
timestamp: integer("timestamp").notNull(), // unix epoch seconds
|
||||
}, (table) => [
|
||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||
]);
|
||||
export const statusHistory = sqliteTable(
|
||||
"statusHistory",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
||||
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
||||
timestamp: integer("timestamp").notNull() // unix epoch seconds
|
||||
},
|
||||
(table) => [
|
||||
index("idx_statusHistory_entity").on(
|
||||
table.entityType,
|
||||
table.entityId,
|
||||
table.timestamp
|
||||
),
|
||||
index("idx_statusHistory_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
@@ -1278,3 +1413,6 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||
|
||||
@@ -24,7 +24,8 @@ export enum TierFeature {
|
||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||
AlertingRules = "alertingRules",
|
||||
WildcardSubdomain = "wildcardSubdomain"
|
||||
WildcardSubdomain = "wildcardSubdomain",
|
||||
ResourcePolicies = "resourcePolicies"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
@@ -66,5 +67,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"]
|
||||
};
|
||||
|
||||
@@ -361,7 +361,7 @@ export async function updateClientResources(
|
||||
} else {
|
||||
let aliasAddress: string | null = null;
|
||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||
}
|
||||
|
||||
let domainInfo:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -162,9 +162,10 @@ export const HeaderSchema = z.object({
|
||||
});
|
||||
|
||||
// Schema for individual resource
|
||||
export const ResourceSchema = z
|
||||
export const PublicResourceSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
policy: z.string().optional(),
|
||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
scheme: z.enum(["http", "https"]).optional(),
|
||||
@@ -340,7 +341,8 @@ export const ResourceSchema = z
|
||||
if (parts.includes("*", 1)) return false; // no further wildcards
|
||||
if (parts.length < 3) return false; // need at least *.label.tld
|
||||
|
||||
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||
const labelRegex =
|
||||
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||
return parts.slice(1).every((label) => labelRegex.test(label));
|
||||
},
|
||||
{
|
||||
@@ -354,7 +356,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
||||
return Object.keys(resource).length === 1 && resource.targets;
|
||||
}
|
||||
|
||||
export const ClientResourceSchema = z
|
||||
export const PrivateResourceSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
mode: z.enum(["host", "cidr", "http"]),
|
||||
@@ -435,19 +437,19 @@ export const ClientResourceSchema = z
|
||||
export const ConfigSchema = z
|
||||
.object({
|
||||
"proxy-resources": z
|
||||
.record(z.string(), ResourceSchema)
|
||||
.record(z.string(), PublicResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"public-resources": z
|
||||
.record(z.string(), ResourceSchema)
|
||||
.record(z.string(), PublicResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"client-resources": z
|
||||
.record(z.string(), ClientResourceSchema)
|
||||
.record(z.string(), PrivateResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"private-resources": z
|
||||
.record(z.string(), ClientResourceSchema)
|
||||
.record(z.string(), PrivateResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||
@@ -472,10 +474,13 @@ export const ConfigSchema = z
|
||||
}
|
||||
|
||||
return data as {
|
||||
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
|
||||
"proxy-resources": Record<
|
||||
string,
|
||||
z.infer<typeof PublicResourceSchema>
|
||||
>;
|
||||
"client-resources": Record<
|
||||
string,
|
||||
z.infer<typeof ClientResourceSchema>
|
||||
z.infer<typeof PrivateResourceSchema>
|
||||
>;
|
||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||
};
|
||||
@@ -614,5 +619,5 @@ export const ConfigSchema = z
|
||||
// Type inference from the schema
|
||||
export type Site = z.infer<typeof SiteSchema>;
|
||||
export type Target = z.infer<typeof TargetSchema>;
|
||||
export type Resource = z.infer<typeof ResourceSchema>;
|
||||
export type Resource = z.infer<typeof PublicResourceSchema>;
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.18.3";
|
||||
export const APP_VERSION = "1.18.2";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
212
server/lib/ip.ts
212
server/lib/ip.ts
@@ -6,7 +6,6 @@ import z from "zod";
|
||||
import logger from "@server/logger";
|
||||
import semver from "semver";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
|
||||
interface IPRange {
|
||||
start: bigint;
|
||||
@@ -328,146 +327,120 @@ export async function getNextAvailableClientSubnet(
|
||||
orgId: string,
|
||||
transaction: Transaction | typeof db = db
|
||||
): Promise<string> {
|
||||
return await lockManager.withLock(
|
||||
`client-subnet-allocation:${orgId}`,
|
||||
async () => {
|
||||
const [org] = await transaction
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
const [org] = await transaction
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
|
||||
if (!org.subnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no subnet defined`
|
||||
);
|
||||
}
|
||||
if (!org.subnet) {
|
||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||
}
|
||||
|
||||
const existingAddressesSites = await transaction
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||
const existingAddressesSites = await transaction
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||
|
||||
const existingAddressesClients = await transaction
|
||||
.select({
|
||||
address: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(
|
||||
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
|
||||
);
|
||||
const existingAddressesClients = await transaction
|
||||
.select({
|
||||
address: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
|
||||
|
||||
const addresses = [
|
||||
...existingAddressesSites.map(
|
||||
(site) => `${site.address?.split("/")[0]}/32`
|
||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||
...existingAddressesClients.map(
|
||||
(client) => `${client.address.split("/")}/32`
|
||||
)
|
||||
].filter((address) => address !== null) as string[];
|
||||
const addresses = [
|
||||
...existingAddressesSites.map(
|
||||
(site) => `${site.address?.split("/")[0]}/32`
|
||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||
...existingAddressesClients.map(
|
||||
(client) => `${client.address.split("/")}/32`
|
||||
)
|
||||
].filter((address) => address !== null) as string[];
|
||||
|
||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
return subnet;
|
||||
}
|
||||
);
|
||||
return subnet;
|
||||
}
|
||||
|
||||
export async function getNextAvailableAliasAddress(
|
||||
orgId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
orgId: string
|
||||
): Promise<string> {
|
||||
return await lockManager.withLock(
|
||||
`alias-address-allocation:${orgId}`,
|
||||
async () => {
|
||||
const [org] = await trx
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
|
||||
if (!org.subnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no subnet defined`
|
||||
);
|
||||
}
|
||||
if (!org.subnet) {
|
||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||
}
|
||||
|
||||
if (!org.utilitySubnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no utility subnet defined`
|
||||
);
|
||||
}
|
||||
if (!org.utilitySubnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no utility subnet defined`
|
||||
);
|
||||
}
|
||||
|
||||
const existingAddresses = await trx
|
||||
.select({
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
isNotNull(siteResources.aliasAddress),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
isNotNull(siteResources.aliasAddress),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
const addresses = [
|
||||
...existingAddresses.map(
|
||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||
),
|
||||
// reserve a /29 for the dns server and other stuff
|
||||
`${org.utilitySubnet.split("/")[0]}/29`
|
||||
].filter((address) => address !== null) as string[];
|
||||
const addresses = [
|
||||
...existingAddresses.map(
|
||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||
),
|
||||
// reserve a /29 for the dns server and other stuff
|
||||
`${org.utilitySubnet.split("/")[0]}/29`
|
||||
].filter((address) => address !== null) as string[];
|
||||
|
||||
let subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
32,
|
||||
org.utilitySubnet
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
// remove the cidr
|
||||
subnet = subnet.split("/")[0];
|
||||
// remove the cidr
|
||||
subnet = subnet.split("/")[0];
|
||||
|
||||
return subnet;
|
||||
}
|
||||
);
|
||||
return subnet;
|
||||
}
|
||||
|
||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||
return await lockManager.withLock("org-subnet-allocation", async () => {
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
subnet: orgs.subnet
|
||||
})
|
||||
.from(orgs)
|
||||
.where(isNotNull(orgs.subnet));
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
subnet: orgs.subnet
|
||||
})
|
||||
.from(orgs)
|
||||
.where(isNotNull(orgs.subnet));
|
||||
|
||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||
|
||||
const subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().orgs.block_size,
|
||||
config.getRawConfig().orgs.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
const subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().orgs.block_size,
|
||||
config.getRawConfig().orgs.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
return subnet;
|
||||
});
|
||||
return subnet;
|
||||
}
|
||||
|
||||
export function generateRemoteSubnets(
|
||||
@@ -505,12 +478,7 @@ export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||
|
||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||
return allSiteResources
|
||||
.filter(
|
||||
(sr) =>
|
||||
sr.aliasAddress &&
|
||||
((sr.alias && sr.mode == "host") ||
|
||||
(sr.fullDomain && sr.mode == "http"))
|
||||
)
|
||||
.filter((sr) => sr.aliasAddress && ((sr.alias && sr.mode == "host") || (sr.fullDomain && sr.mode == "http")))
|
||||
.map((sr) => ({
|
||||
alias: sr.alias || sr.fullDomain,
|
||||
aliasAddress: sr.aliasAddress
|
||||
|
||||
@@ -24,11 +24,8 @@ export async function getCachedStatusHistory(
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Anchor to UTC midnight so the query window aligns with stable calendar days
|
||||
const utcToday = new Date();
|
||||
utcToday.setUTCHours(0, 0, 0, 0);
|
||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
||||
const startSec = todayMidnightSec - days * 86400;
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const startSec = nowSec - days * 86400;
|
||||
|
||||
const events = await logsDb
|
||||
.select()
|
||||
@@ -113,18 +110,11 @@ export function computeBuckets(
|
||||
days: number
|
||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
|
||||
// and don't drift as the cache expires and is recomputed
|
||||
const utcToday = new Date();
|
||||
utcToday.setUTCHours(0, 0, 0, 0);
|
||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
||||
|
||||
const buckets: StatusHistoryDayBucket[] = [];
|
||||
let totalDowntime = 0;
|
||||
|
||||
for (let d = 0; d < days; d++) {
|
||||
const dayStartSec = todayMidnightSec - (days - d) * 86400;
|
||||
const dayStartSec = nowSec - (days - d) * 86400;
|
||||
const dayEndSec = dayStartSec + 86400;
|
||||
|
||||
const dayEvents = events.filter(
|
||||
|
||||
@@ -32,3 +32,4 @@ export * from "./verifySiteResourceAccess";
|
||||
export * from "./logActionAudit";
|
||||
export * from "./verifyOlmAccess";
|
||||
export * from "./verifyLimits";
|
||||
export * from "./verifyResourcePolicyAccess";
|
||||
|
||||
@@ -16,3 +16,4 @@ export * from "./verifyApiKeyClientAccess";
|
||||
export * from "./verifyApiKeySiteResourceAccess";
|
||||
export * from "./verifyApiKeyIdpAccess";
|
||||
export * from "./verifyApiKeyDomainAccess";
|
||||
export * from "./verifyApiKeyResourcePolicyAccess";
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourcePolicies, apiKeyOrg } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyResourcePolicyAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const apiKey = req.apiKey;
|
||||
const resourcePolicyId =
|
||||
req.params.resourcePolicyId ||
|
||||
req.body.resourcePolicyId ||
|
||||
req.query.resourcePolicyId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve the resource policy
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||
.limit(1);
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource policy with ID ${resourcePolicyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKey.isRoot) {
|
||||
// Root keys can access any resource policy in any org
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!policy.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Resource policy with ID ${resourcePolicyId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the API key is linked to the resource policy's organization
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgResult = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, policy.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (apiKeyOrgResult.length > 0) {
|
||||
req.apiKeyOrg = apiKeyOrgResult[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 resource policy access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
127
server/middlewares/verifyResourcePolicyAccess.ts
Normal file
127
server/middlewares/verifyResourcePolicyAccess.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourcePolicies, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||
|
||||
export async function verifyResourcePolicyAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const userId = req.user!.userId;
|
||||
const resourcePolicyIdStr =
|
||||
req.params?.resourcePolicyId ||
|
||||
req.body?.resourcePolicyId ||
|
||||
req.query?.resourcePolicyId;
|
||||
const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId;
|
||||
const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId;
|
||||
|
||||
try {
|
||||
if (!userId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
let policy: typeof resourcePolicies.$inferSelect | null = null;
|
||||
|
||||
if (orgId && niceId) {
|
||||
const [policyRes] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicies.niceId, niceId),
|
||||
eq(resourcePolicies.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
policy = policyRes ?? null;
|
||||
} else {
|
||||
const resourcePolicyId = parseInt(resourcePolicyIdStr);
|
||||
if (isNaN(resourcePolicyId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid resource policy ID"
|
||||
)
|
||||
);
|
||||
}
|
||||
const [policyRes] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||
.limit(1);
|
||||
policy = policyRes ?? null;
|
||||
}
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource policy with ID ${resourcePolicyIdStr ?? niceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
const userOrgRes = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, policy.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
req.userOrg = userOrgRes[0];
|
||||
}
|
||||
|
||||
if (!req.userOrg || req.userOrg.orgId !== policy.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||
const policyCheck = await checkOrgAccessPolicy({
|
||||
orgId: req.userOrg.orgId,
|
||||
userId,
|
||||
session: req.session
|
||||
});
|
||||
req.orgPolicyAllowed = policyCheck.allowed;
|
||||
if (!policyCheck.allowed || policyCheck.error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
req.userOrgRoleIds = await getUserOrgRoleIds(
|
||||
req.userOrg.userId,
|
||||
policy.orgId
|
||||
);
|
||||
req.userOrgId = policy.orgId;
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying resource policy access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function verifyUserCanSetUserOrgRoles() {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
"User does not have permission to set user organization roles"
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,6 +7,7 @@ export enum OpenAPITags {
|
||||
Org = "Organization",
|
||||
PublicResource = "Public Resource",
|
||||
PrivateResource = "Private Resource",
|
||||
Policy = "Policy",
|
||||
Role = "Role",
|
||||
User = "User",
|
||||
Invitation = "User Invitation",
|
||||
|
||||
@@ -29,10 +29,7 @@ import { decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import { sendAlertWebhook } from "./sendAlertWebhook";
|
||||
import { sendAlertEmail } from "./sendAlertEmail";
|
||||
import {
|
||||
AlertContext,
|
||||
WebhookAlertConfig
|
||||
} from "@server/routers/alertRule/types";
|
||||
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||
|
||||
/**
|
||||
* Core alert processing pipeline.
|
||||
@@ -102,10 +99,7 @@ export async function processAlerts(context: AlertContext): Promise<void> {
|
||||
baseConditions,
|
||||
or(
|
||||
eq(alertRules.allHealthChecks, true),
|
||||
eq(
|
||||
alertHealthChecks.healthCheckId,
|
||||
context.healthCheckId
|
||||
)
|
||||
eq(alertHealthChecks.healthCheckId, context.healthCheckId)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -214,19 +208,14 @@ async function processRule(
|
||||
|
||||
for (const action of emailActions) {
|
||||
try {
|
||||
const recipients = await resolveEmailRecipients(
|
||||
action.emailActionId
|
||||
);
|
||||
const recipients = await resolveEmailRecipients(action.emailActionId);
|
||||
if (recipients.length > 0) {
|
||||
await sendAlertEmail(recipients, context);
|
||||
await db
|
||||
.update(alertEmailActions)
|
||||
.set({ lastSentAt: now })
|
||||
.where(
|
||||
eq(
|
||||
alertEmailActions.emailActionId,
|
||||
action.emailActionId
|
||||
)
|
||||
eq(alertEmailActions.emailActionId, action.emailActionId)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -280,7 +269,7 @@ async function processRule(
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
logger.error(
|
||||
`processAlerts: failed to send alert webhook for action ${action.webhookActionId}`,
|
||||
err
|
||||
);
|
||||
@@ -300,9 +289,7 @@ async function processRule(
|
||||
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
||||
* - Direct external email addresses
|
||||
*/
|
||||
async function resolveEmailRecipients(
|
||||
emailActionId: number
|
||||
): Promise<string[]> {
|
||||
async function resolveEmailRecipients(emailActionId: number): Promise<string[]> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(alertEmailRecipients)
|
||||
|
||||
@@ -236,43 +236,15 @@ interface TemplateContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a body template with {{event}}, {{timestamp}}, {{status}}, {{data}},
|
||||
* and individual data-field placeholders (e.g. {{orgId}}, {{siteId}}, …).
|
||||
* Render a body template with {{event}}, {{timestamp}}, {{status}}, and
|
||||
* {{data}} placeholders, mirroring the logic in HttpLogDestination.
|
||||
*
|
||||
* Replacement order:
|
||||
* 1. {{data}} → raw JSON of the full data object (prevents re-expansion of
|
||||
* nested values that might look like placeholders).
|
||||
* 2. Top-level scalar fields from data (string values are JSON-escaped;
|
||||
* numbers and booleans are rendered as-is). Unknown placeholders are
|
||||
* left untouched.
|
||||
* 3. The fixed top-level keys: event, timestamp, status.
|
||||
* {{data}} is replaced first (as raw JSON) so that any literal "{{…}}"
|
||||
* strings inside data values are not re-expanded.
|
||||
*/
|
||||
function renderTemplate(template: string, ctx: TemplateContext): string {
|
||||
// Step 1 – expand {{data}} first so its contents are already serialised
|
||||
// and won't be touched by later passes.
|
||||
let rendered = template.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data));
|
||||
|
||||
// Step 2 – expand individual data fields. Only replace placeholders whose
|
||||
// key actually exists in ctx.data; leave everything else as-is.
|
||||
for (const [key, value] of Object.entries(ctx.data)) {
|
||||
if (value === null || value === undefined) continue;
|
||||
const placeholder = new RegExp(
|
||||
`\\{\\{${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\}\\}`,
|
||||
"g"
|
||||
);
|
||||
let serialised: string;
|
||||
if (typeof value === "string") {
|
||||
serialised = escapeJsonString(value);
|
||||
} else if (typeof value === "number" || typeof value === "boolean") {
|
||||
serialised = String(value);
|
||||
} else {
|
||||
serialised = escapeJsonString(JSON.stringify(value));
|
||||
}
|
||||
rendered = rendered.replace(placeholder, serialised);
|
||||
}
|
||||
|
||||
// Step 3 – expand the fixed top-level keys.
|
||||
rendered = rendered
|
||||
const rendered = template
|
||||
.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data))
|
||||
.replace(/\{\{event\}\}/g, escapeJsonString(ctx.event))
|
||||
.replace(/\{\{timestamp\}\}/g, escapeJsonString(ctx.timestamp))
|
||||
.replace(/\{\{status\}\}/g, escapeJsonString(ctx.status));
|
||||
|
||||
@@ -31,6 +31,8 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||
import * as alertRule from "#private/routers/alertRule";
|
||||
import * as healthChecks from "#private/routers/healthChecks";
|
||||
import * as resource from "#private/routers/resource";
|
||||
import * as policy from "#private/routers/policy";
|
||||
|
||||
import {
|
||||
verifyOrgAccess,
|
||||
@@ -44,7 +46,8 @@ import {
|
||||
verifyUserCanSetUserOrgRoles,
|
||||
verifySiteProvisioningKeyAccess,
|
||||
verifyIsLoggedInUser,
|
||||
verifyAdmin
|
||||
verifyAdmin,
|
||||
verifyResourcePolicyAccess
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import {
|
||||
@@ -382,6 +385,39 @@ authenticated.get(
|
||||
approval.countApprovals
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourcePolicy),
|
||||
logActionAudit(ActionsEnum.deleteResourcePolicy),
|
||||
policy.deleteResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resource-policies",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.listResourcePolicies),
|
||||
logActionAudit(ActionsEnum.listResourcePolicies),
|
||||
policy.listResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/resource-policy",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createResourcePolicy),
|
||||
logActionAudit(ActionsEnum.createResourcePolicy),
|
||||
policy.createResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/approvals/:approvalId",
|
||||
verifyValidLicense,
|
||||
|
||||
@@ -45,8 +45,11 @@ import {
|
||||
users,
|
||||
userOrgs,
|
||||
roleResources,
|
||||
rolePolicies,
|
||||
userResources,
|
||||
userPolicies,
|
||||
resourceRules,
|
||||
resourcePolicyRules,
|
||||
userOrgRoles,
|
||||
roles
|
||||
} from "@server/db";
|
||||
@@ -430,7 +433,10 @@ hybridRouter.get(
|
||||
);
|
||||
|
||||
// Decrypt and save key file
|
||||
const decryptedKey = decrypt(cert.keyFile!, config.getRawConfig().server.secret!);
|
||||
const decryptedKey = decrypt(
|
||||
cert.keyFile!,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
|
||||
// Return only the certificate data without org information
|
||||
return {
|
||||
@@ -531,7 +537,10 @@ hybridRouter.get(
|
||||
wildcardCandidates.length > 0
|
||||
? and(
|
||||
eq(resources.wildcard, true),
|
||||
inArray(resources.fullDomain, wildcardCandidates)
|
||||
inArray(
|
||||
resources.fullDomain,
|
||||
wildcardCandidates
|
||||
)
|
||||
)
|
||||
: sql`false`
|
||||
)
|
||||
@@ -545,10 +554,10 @@ hybridRouter.get(
|
||||
|
||||
if (
|
||||
result &&
|
||||
await checkExitNodeOrg(
|
||||
(await checkExitNodeOrg(
|
||||
remoteExitNode.exitNodeId,
|
||||
result.resources.orgId
|
||||
)
|
||||
))
|
||||
) {
|
||||
// If the exit node is not allowed for the org, return an error
|
||||
return next(
|
||||
@@ -1132,22 +1141,43 @@ hybridRouter.get(
|
||||
);
|
||||
}
|
||||
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
const [direct, viaPolicies] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
.limit(1),
|
||||
db
|
||||
.select({
|
||||
roleId: rolePolicies.roleId,
|
||||
resourcePolicyId: rolePolicies.resourcePolicyId
|
||||
})
|
||||
.from(rolePolicies)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(
|
||||
resources.resourcePolicyId,
|
||||
rolePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(rolePolicies.roleId, roleId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
]);
|
||||
|
||||
const result =
|
||||
roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
||||
const result = direct[0] ?? viaPolicies[0] ?? null;
|
||||
|
||||
return response<typeof roleResources.$inferSelect | null>(res, {
|
||||
data: result,
|
||||
data: result as any,
|
||||
success: true,
|
||||
error: false,
|
||||
message: result
|
||||
@@ -1222,21 +1252,44 @@ hybridRouter.get(
|
||||
);
|
||||
}
|
||||
|
||||
const roleResourceAccess = await db
|
||||
.select({
|
||||
resourceId: roleResources.resourceId,
|
||||
roleId: roleResources.roleId
|
||||
})
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
);
|
||||
const [direct, viaPolicies] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
resourceId: roleResources.resourceId,
|
||||
roleId: roleResources.roleId
|
||||
})
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
),
|
||||
roleIds.length > 0
|
||||
? db
|
||||
.select({
|
||||
resourceId: sql<number>`${resourceId}`,
|
||||
roleId: rolePolicies.roleId
|
||||
})
|
||||
.from(rolePolicies)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(
|
||||
resources.resourcePolicyId,
|
||||
rolePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
inArray(rolePolicies.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
const result =
|
||||
roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||
const combined = [...direct, ...viaPolicies];
|
||||
const result = combined.length > 0 ? combined : null;
|
||||
|
||||
return response<{ resourceId: number; roleId: number }[] | null>(
|
||||
res,
|
||||
@@ -1397,10 +1450,45 @@ hybridRouter.get(
|
||||
);
|
||||
}
|
||||
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
const [directRules, policyRules] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId)),
|
||||
db
|
||||
.select({
|
||||
ruleId: resourcePolicyRules.ruleId,
|
||||
resourceId: sql<number>`${resourceId}`,
|
||||
enabled: resourcePolicyRules.enabled,
|
||||
priority: resourcePolicyRules.priority,
|
||||
action: resourcePolicyRules.action,
|
||||
match: resourcePolicyRules.match,
|
||||
value: resourcePolicyRules.value
|
||||
})
|
||||
.from(resourcePolicyRules)
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(
|
||||
resources.resourcePolicyId,
|
||||
resourcePolicyRules.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
]);
|
||||
|
||||
const maxDirectPriority = directRules.reduce(
|
||||
(max, r) => Math.max(max, r.priority),
|
||||
0
|
||||
);
|
||||
const offsetPolicyRules = policyRules.map((r) => ({
|
||||
...r,
|
||||
priority: maxDirectPriority + r.priority
|
||||
}));
|
||||
|
||||
const rules = [
|
||||
...directRules,
|
||||
...offsetPolicyRules
|
||||
] as (typeof resourceRules.$inferSelect)[];
|
||||
|
||||
// backward compatibility: COUNTRY -> GEOIP
|
||||
// TODO: remove this after a few versions once all exit nodes are updated
|
||||
|
||||
417
server/private/routers/policy/createResourcePolicy.ts
Normal file
417
server/private/routers/policy/createResourcePolicy.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import {
|
||||
db,
|
||||
idp,
|
||||
idpOrg,
|
||||
orgs,
|
||||
resourcePolicies,
|
||||
resourcePolicyHeaderAuth,
|
||||
resourcePolicyPassword,
|
||||
resourcePolicyPincode,
|
||||
resourcePolicyRules,
|
||||
resourcePolicyWhiteList,
|
||||
rolePolicies,
|
||||
roles,
|
||||
userOrgs,
|
||||
userPolicies,
|
||||
users,
|
||||
type ResourcePolicy
|
||||
} from "@server/db";
|
||||
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||
import response from "@server/lib/response";
|
||||
import {
|
||||
isValidCIDR,
|
||||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, inArray, type InferInsertModel } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import z from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const createResourcePolicyParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const ruleSchema = z.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||
type: "string",
|
||||
enum: ["ACCEPT", "DROP", "PASS"],
|
||||
description: "rule action"
|
||||
}),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]).openapi({
|
||||
type: "string",
|
||||
enum: ["CIDR", "IP", "PATH"],
|
||||
description: "rule match"
|
||||
}),
|
||||
value: z.string().min(1),
|
||||
priority: z.int().openapi({
|
||||
type: "integer",
|
||||
description: "Rule priority"
|
||||
}),
|
||||
enabled: z.boolean().optional()
|
||||
});
|
||||
|
||||
const createResourcePolicyBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
// Access control
|
||||
sso: z.boolean().default(true),
|
||||
skipToIdpId: z
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.nullable()
|
||||
.openapi({ type: "integer" }),
|
||||
roleIds: z
|
||||
.array(z.string().transform(Number).pipe(z.int().positive()))
|
||||
.optional()
|
||||
.default([]),
|
||||
userIds: z.array(z.string()).optional().default([]),
|
||||
// auth methods
|
||||
password: z.string().min(4).max(100).nullable().optional(),
|
||||
pincode: z
|
||||
.string()
|
||||
.regex(/^\d{6}$/)
|
||||
.or(z.null())
|
||||
.optional(),
|
||||
headerAuth: z
|
||||
.object({
|
||||
user: z.string().min(4).max(100),
|
||||
password: z.string().min(4).max(100),
|
||||
extendedCompatibility: z.boolean()
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
// email OTP
|
||||
emailWhitelistEnabled: z.boolean().optional().default(false),
|
||||
emails: z
|
||||
.array(
|
||||
z.email().or(
|
||||
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||
error: "Invalid email address. Wildcard (*) must be the entire local part."
|
||||
})
|
||||
)
|
||||
)
|
||||
.max(50)
|
||||
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||
.optional()
|
||||
.default([]),
|
||||
// rules
|
||||
applyRules: z.boolean().default(false),
|
||||
rules: z.array(ruleSchema).optional().default([])
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/resource-policy",
|
||||
description: "Create a resource policy.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: createResourcePolicyParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createResourcePolicyBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createResourcePolicy(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
// Validate request params
|
||||
const parsedParams = createResourcePolicyParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (req.user && req.userOrgRoleIds?.length === 0) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
}
|
||||
|
||||
// get the org
|
||||
const org = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (org.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Organization with ID ${orgId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = createResourcePolicyBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
sso,
|
||||
userIds,
|
||||
roleIds,
|
||||
skipToIdpId,
|
||||
applyRules,
|
||||
emailWhitelistEnabled,
|
||||
password,
|
||||
pincode,
|
||||
headerAuth,
|
||||
emails,
|
||||
rules
|
||||
} = parsedBody.data;
|
||||
|
||||
// Check if Identity provider in `skipToIdpId` exists
|
||||
if (skipToIdpId) {
|
||||
const [provider] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
|
||||
.where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!provider) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Identity provider not found in this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const adminRole = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (adminRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||
);
|
||||
}
|
||||
|
||||
const existingRoles = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(inArray(roles.roleId, roleIds)));
|
||||
|
||||
const hasAdminRole = existingRoles.some((role) => role.isAdmin);
|
||||
|
||||
if (hasAdminRole) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Admin role cannot be assigned to resource policy"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const existingUsers = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.innerJoin(userOrgs, eq(userOrgs.userId, users.userId))
|
||||
.where(
|
||||
and(eq(userOrgs.orgId, orgId), inArray(users.userId, userIds))
|
||||
);
|
||||
|
||||
const niceId = await getUniqueResourcePolicyName(orgId);
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
} else if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||
);
|
||||
} else if (
|
||||
rule.match === "PATH" &&
|
||||
!isValidUrlGlobPattern(rule.value)
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const policy = await db.transaction(async (trx) => {
|
||||
const [newPolicy] = await trx
|
||||
.insert(resourcePolicies)
|
||||
.values({
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
sso,
|
||||
idpId: skipToIdpId,
|
||||
applyRules,
|
||||
emailWhitelistEnabled
|
||||
})
|
||||
.returning();
|
||||
|
||||
const rolesToAdd = [
|
||||
{
|
||||
roleId: adminRole[0].roleId,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
}
|
||||
] satisfies InferInsertModel<typeof rolePolicies>[];
|
||||
|
||||
rolesToAdd.push(
|
||||
...existingRoles.map((role) => ({
|
||||
roleId: role.roleId,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
}))
|
||||
);
|
||||
|
||||
await trx.insert(rolePolicies).values(rolesToAdd);
|
||||
|
||||
const usersToAdd: InferInsertModel<typeof userPolicies>[] = [];
|
||||
|
||||
if (
|
||||
req.user &&
|
||||
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
|
||||
) {
|
||||
// make sure the user can access the policy
|
||||
usersToAdd.push({
|
||||
userId: req.user?.userId!,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
});
|
||||
}
|
||||
|
||||
usersToAdd.push(
|
||||
...existingUsers.map(({ user }) => ({
|
||||
userId: user.userId,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
}))
|
||||
);
|
||||
|
||||
if (usersToAdd.length > 0) {
|
||||
await trx.insert(userPolicies).values(usersToAdd);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
await trx.insert(resourcePolicyPassword).values({
|
||||
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||
passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
if (pincode) {
|
||||
const pincodeHash = await hashPassword(pincode);
|
||||
|
||||
await trx.insert(resourcePolicyPincode).values({
|
||||
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||
pincodeHash,
|
||||
digitLength: 6
|
||||
});
|
||||
}
|
||||
|
||||
if (headerAuth) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
);
|
||||
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||
headerAuthHash,
|
||||
extendedCompatibility: headerAuth.extendedCompatibility
|
||||
});
|
||||
}
|
||||
|
||||
if (emailWhitelistEnabled && emails.length > 0) {
|
||||
await trx.insert(resourcePolicyWhiteList).values(
|
||||
emails.map((email) => ({
|
||||
email,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (rules.length > 0) {
|
||||
await trx.insert(resourcePolicyRules).values(
|
||||
rules.map((rule) => ({
|
||||
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||
...rule
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return newPolicy;
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create policy"
|
||||
)
|
||||
);
|
||||
}
|
||||
return response<ResourcePolicy>(res, {
|
||||
data: policy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "resource policy created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
107
server/private/routers/policy/deleteResourcePolicy.ts
Normal file
107
server/private/routers/policy/deleteResourcePolicy.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { db, resourcePolicies, resources } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import z from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const deleteResourcePolicySchema = z.strictObject({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Delete a resource policy.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: deleteResourcePolicySchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function deleteResourcePolicy(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = deleteResourcePolicySchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
|
||||
const [existingResource] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
if (!existingResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource Policy with ID ${resourcePolicyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const totalAffectedResources = await db.$count(
|
||||
db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourcePolicyId, resourcePolicyId))
|
||||
);
|
||||
|
||||
if (totalAffectedResources > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Cannot delete Policy '${existingResource.name}' as it's being used by at least one resource`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// delete policy
|
||||
await db
|
||||
.delete(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource Policy deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
16
server/private/routers/policy/index.ts
Normal file
16
server/private/routers/policy/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./createResourcePolicy";
|
||||
export * from "./listResourcePolicies";
|
||||
export * from "./deleteResourcePolicy";
|
||||
271
server/private/routers/policy/listResourcePolicies.ts
Normal file
271
server/private/routers/policy/listResourcePolicies.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import {
|
||||
db,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
rolePolicies,
|
||||
userPolicies
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import type {
|
||||
ListResourcePoliciesResponse,
|
||||
ResourcePolicyWithResources
|
||||
} from "@server/routers/resource/types";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, asc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
|
||||
const listResourcePoliciesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listResourcePoliciesSchema = z.object({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 20,
|
||||
description: "Number of items per page"
|
||||
}),
|
||||
page: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.catch(1)
|
||||
.default(1)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
}),
|
||||
query: z.string().optional()
|
||||
});
|
||||
|
||||
function queryResourcePoliciesBase() {
|
||||
return db
|
||||
.select({
|
||||
resourcePolicyId: resourcePolicies.resourcePolicyId,
|
||||
name: resourcePolicies.name,
|
||||
niceId: resourcePolicies.niceId,
|
||||
orgId: resourcePolicies.orgId
|
||||
})
|
||||
.from(resourcePolicies);
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resource-policies",
|
||||
description: "List resource policies for an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
}),
|
||||
query: listResourcePoliciesSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourcePolicies(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listResourcePoliciesSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { page, pageSize, query } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listResourcePoliciesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgId =
|
||||
parsedParams.data.orgId ||
|
||||
req.userOrg?.orgId ||
|
||||
req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let accessibleResourcePolicies: Array<{ resourcePolicyId: number }>;
|
||||
if (req.user) {
|
||||
accessibleResourcePolicies = await db
|
||||
.select({
|
||||
resourcePolicyId: sql<number>`COALESCE(${userPolicies.resourcePolicyId}, ${rolePolicies.resourcePolicyId})`
|
||||
})
|
||||
.from(userPolicies)
|
||||
.fullJoin(
|
||||
rolePolicies,
|
||||
eq(
|
||||
userPolicies.resourcePolicyId,
|
||||
rolePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
eq(userPolicies.userId, req.user!.userId),
|
||||
inArray(rolePolicies.roleId, req.userOrgRoleIds || [])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleResourcePolicies = await db
|
||||
.select({
|
||||
resourcePolicyId: resourcePolicies.resourcePolicyId
|
||||
})
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleResourceIds = accessibleResourcePolicies.map(
|
||||
(resource) => resource.resourcePolicyId
|
||||
);
|
||||
|
||||
const conditions = [
|
||||
and(
|
||||
inArray(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
accessibleResourceIds
|
||||
),
|
||||
eq(resourcePolicies.orgId, orgId),
|
||||
eq(resourcePolicies.scope, "global")
|
||||
)
|
||||
];
|
||||
|
||||
if (query) {
|
||||
conditions.push(
|
||||
or(
|
||||
like(
|
||||
sql`LOWER(${resourcePolicies.name})`,
|
||||
"%" + query.toLowerCase() + "%"
|
||||
),
|
||||
like(
|
||||
sql`LOWER(${resourcePolicies.niceId})`,
|
||||
"%" + query.toLowerCase() + "%"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const baseQuery = queryResourcePoliciesBase().where(and(...conditions));
|
||||
|
||||
// we need to add `as` so that drizzle filters the result as a subquery
|
||||
const countQuery = db.$count(baseQuery.as("filtered_policies"));
|
||||
|
||||
const [rows, totalCount] = await Promise.all([
|
||||
baseQuery
|
||||
.limit(pageSize)
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(asc(resourcePolicies.resourcePolicyId)),
|
||||
countQuery
|
||||
]);
|
||||
|
||||
const attachedResources =
|
||||
rows.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select({
|
||||
resourceId: resources.resourceId,
|
||||
name: resources.name,
|
||||
fullDomain: resources.fullDomain,
|
||||
resourcePolicyId: resources.resourcePolicyId
|
||||
})
|
||||
.from(resources)
|
||||
.where(
|
||||
inArray(
|
||||
resources.resourcePolicyId,
|
||||
rows.map((row) => row.resourcePolicyId)
|
||||
)
|
||||
);
|
||||
|
||||
// avoids TS issues with reduce/never[]
|
||||
const map = new Map<number, ResourcePolicyWithResources>();
|
||||
|
||||
for (const row of rows) {
|
||||
let entry = map.get(row.resourcePolicyId);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
...row,
|
||||
resources: []
|
||||
};
|
||||
map.set(row.resourcePolicyId, entry);
|
||||
}
|
||||
|
||||
entry.resources = attachedResources.filter(
|
||||
(r) => r.resourcePolicyId === entry?.resourcePolicyId
|
||||
);
|
||||
}
|
||||
|
||||
const policiesList = Array.from(map.values());
|
||||
|
||||
return response<ListResourcePoliciesResponse>(res, {
|
||||
data: {
|
||||
policies: policiesList,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
pageSize,
|
||||
page
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resources retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -671,7 +671,8 @@ export async function verifyResourceSession(
|
||||
resourceData.org
|
||||
);
|
||||
|
||||
localCache.set(userAccessCacheKey, allowedUserData, 5);
|
||||
// this is query intensive so let it cache a little longer
|
||||
localCache.set(userAccessCacheKey, allowedUserData, 12);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1003,11 +1004,7 @@ async function checkRules(
|
||||
isIpInCidr(clientIp, rule.value)
|
||||
) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "IP" &&
|
||||
clientIp == rule.value
|
||||
) {
|
||||
} else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
path &&
|
||||
@@ -1015,10 +1012,7 @@ async function checkRules(
|
||||
isPathAllowed(rule.value, path)
|
||||
) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "COUNTRY"
|
||||
) {
|
||||
} else if (clientIp && rule.match == "COUNTRY") {
|
||||
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
|
||||
if (
|
||||
rule.value.toUpperCase() === "ALL" &&
|
||||
@@ -1030,10 +1024,7 @@ async function checkRules(
|
||||
if (await isIpInGeoIP(ipCC, rule.value)) {
|
||||
return rule.action as any;
|
||||
}
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "ASN"
|
||||
) {
|
||||
} else if (clientIp && rule.match == "ASN") {
|
||||
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
|
||||
if (
|
||||
(rule.value.toUpperCase() === "ALL" ||
|
||||
@@ -1272,11 +1263,15 @@ export async function isIpInRegion(
|
||||
if (region.id === checkRegionCode) {
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.countries.includes(upperCode)) {
|
||||
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
|
||||
logger.debug(
|
||||
`Country ${upperCode} is in region ${region.id} (${region.name})`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
|
||||
logger.debug(
|
||||
`Country ${upperCode} is not in region ${region.id} (${region.name})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1284,10 +1279,14 @@ export async function isIpInRegion(
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.id === checkRegionCode) {
|
||||
if (subregion.countries.includes(upperCode)) {
|
||||
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
|
||||
logger.debug(
|
||||
`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
|
||||
logger.debug(
|
||||
`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import config from "@server/lib/config";
|
||||
import * as site from "./site";
|
||||
import * as org from "./org";
|
||||
import * as resource from "./resource";
|
||||
import * as policy from "./policy";
|
||||
import * as domain from "./domain";
|
||||
import * as target from "./target";
|
||||
import * as user from "./user";
|
||||
@@ -42,7 +43,8 @@ import {
|
||||
verifyUserIsOrgOwner,
|
||||
verifySiteResourceAccess,
|
||||
verifyOlmAccess,
|
||||
verifyLimits
|
||||
verifyLimits,
|
||||
verifyResourcePolicyAccess
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
@@ -103,7 +105,6 @@ authenticated.put(
|
||||
site.createSite
|
||||
);
|
||||
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/sites",
|
||||
verifyOrgAccess,
|
||||
@@ -540,6 +541,7 @@ authenticated.get(
|
||||
verifyUserHasAction(ActionsEnum.getResource),
|
||||
resource.getResource
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
@@ -646,6 +648,29 @@ authenticated.post(
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resource-policy/:niceId",
|
||||
verifyOrgAccess,
|
||||
verifyResourcePolicyAccess,
|
||||
verifyUserHasAction(ActionsEnum.getResourcePolicy),
|
||||
policy.getResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.getResourcePolicy),
|
||||
resource.getResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResourcePolicy),
|
||||
policy.updateResourcePolicy
|
||||
);
|
||||
|
||||
// authenticated.get(
|
||||
// "/role/:roleId",
|
||||
// verifyRoleAccess,
|
||||
@@ -697,6 +722,59 @@ authenticated.post(
|
||||
resource.setResourceUsers
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/access-control",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyUsers),
|
||||
policy.setResourcePolicyAccessControl
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/password",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPassword),
|
||||
policy.setResourcePolicyPassword
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/pincode",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPincode),
|
||||
policy.setResourcePolicyPincode
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/header-auth",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
policy.setResourcePolicyHeaderAuth
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/whitelist",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
|
||||
policy.setResourcePolicyWhitelist
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/rules",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyRules),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyRules),
|
||||
policy.setResourcePolicyRules
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyResourceAccess,
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as site from "./site";
|
||||
import * as org from "./org";
|
||||
import * as blueprints from "./blueprints";
|
||||
import * as resource from "./resource";
|
||||
import * as policy from "./policy";
|
||||
import * as domain from "./domain";
|
||||
import * as target from "./target";
|
||||
import * as user from "./user";
|
||||
@@ -29,7 +30,9 @@ import {
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyApiKeyDomainAccess
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyUserHasAction
|
||||
} from "@server/middlewares";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Router } from "express";
|
||||
@@ -459,6 +462,20 @@ authenticated.get(
|
||||
resource.getResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||
policy.getResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||
resource.getResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
@@ -468,6 +485,13 @@ authenticated.post(
|
||||
resource.updateResource
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
|
||||
policy.updateResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
@@ -619,6 +643,63 @@ authenticated.post(
|
||||
resource.setResourceUsers
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/access-control",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
|
||||
verifyUserHasAction(ActionsEnum.setResourcePolicyRoles),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyUsers),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyRoles),
|
||||
policy.setResourcePolicyAccessControl
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/password",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPassword),
|
||||
policy.setResourcePolicyPassword
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/pincode",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyPincode),
|
||||
policy.setResourcePolicyPincode
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/header-auth",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||
policy.setResourcePolicyHeaderAuth
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/whitelist",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
|
||||
policy.setResourcePolicyWhitelist
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId/rules",
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
|
||||
logActionAudit(ActionsEnum.setResourcePolicyRules),
|
||||
policy.setResourcePolicyRules
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles/add",
|
||||
verifyApiKeyResourceAccess,
|
||||
|
||||
231
server/routers/policy/getResourcePolicy.ts
Normal file
231
server/routers/policy/getResourcePolicy.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
db,
|
||||
idp,
|
||||
resourcePolicyRules,
|
||||
resourcePolicies,
|
||||
resourcePolicyHeaderAuth,
|
||||
resourcePolicyPassword,
|
||||
resourcePolicyPincode,
|
||||
resourcePolicyWhiteList,
|
||||
rolePolicies,
|
||||
roles,
|
||||
userPolicies,
|
||||
users
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull, not, or, type SQL } from "drizzle-orm";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import z from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const getResourcePolicySchema = z
|
||||
.strictObject({
|
||||
niceId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.or(
|
||||
z.strictObject({
|
||||
resourcePolicyId: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
.positive()
|
||||
.openapi({
|
||||
type: "integer",
|
||||
description: "Resource policy ID"
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export async function queryResourcePolicy(
|
||||
params: z.infer<typeof getResourcePolicySchema>
|
||||
) {
|
||||
const conditions: SQL<unknown>[] = [];
|
||||
if ("resourcePolicyId" in params) {
|
||||
conditions.push(
|
||||
eq(resourcePolicies.resourcePolicyId, params.resourcePolicyId)
|
||||
);
|
||||
} else {
|
||||
conditions.push(
|
||||
eq(resourcePolicies.niceId, params.niceId),
|
||||
eq(resourcePolicies.orgId, params.orgId)
|
||||
);
|
||||
}
|
||||
|
||||
const [res] = await db
|
||||
.select({
|
||||
resourcePolicyId: resourcePolicies.resourcePolicyId,
|
||||
sso: resourcePolicies.sso,
|
||||
applyRules: resourcePolicies.applyRules,
|
||||
emailWhitelistEnabled: resourcePolicies.emailWhitelistEnabled,
|
||||
idpId: resourcePolicies.idpId,
|
||||
niceId: resourcePolicies.niceId,
|
||||
name: resourcePolicies.name,
|
||||
passwordId: resourcePolicyPassword.passwordId,
|
||||
pincodeId: resourcePolicyPincode.pincodeId,
|
||||
headerAuth: {
|
||||
id: resourcePolicyHeaderAuth.headerAuthId,
|
||||
extendedCompability:
|
||||
resourcePolicyHeaderAuth.extendedCompatibility
|
||||
}
|
||||
})
|
||||
.from(resourcePolicies)
|
||||
.leftJoin(
|
||||
resourcePolicyPassword,
|
||||
eq(
|
||||
resourcePolicyPassword.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyPincode,
|
||||
eq(
|
||||
resourcePolicyPincode.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyHeaderAuth,
|
||||
eq(
|
||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.limit(1);
|
||||
|
||||
if (!res) return null;
|
||||
|
||||
const policyUsers = await db
|
||||
.select({
|
||||
userId: userPolicies.userId,
|
||||
email: users.email,
|
||||
name: users.name,
|
||||
username: users.username,
|
||||
type: users.type,
|
||||
idpName: idp.name
|
||||
})
|
||||
.from(userPolicies)
|
||||
.innerJoin(users, eq(userPolicies.userId, users.userId))
|
||||
.leftJoin(idp, eq(idp.idpId, users.idpId))
|
||||
.where(eq(userPolicies.resourcePolicyId, res.resourcePolicyId));
|
||||
|
||||
const policyRoles = await db
|
||||
.select({
|
||||
roleId: rolePolicies.roleId,
|
||||
name: roles.name
|
||||
})
|
||||
.from(rolePolicies)
|
||||
.innerJoin(
|
||||
roles,
|
||||
and(
|
||||
eq(rolePolicies.roleId, roles.roleId),
|
||||
or(isNull(roles.isAdmin), not(roles.isAdmin))
|
||||
)
|
||||
)
|
||||
.where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId));
|
||||
|
||||
const policyEmailWhiteList = await db
|
||||
.select({
|
||||
whiteListId: resourcePolicyWhiteList.whitelistId,
|
||||
email: resourcePolicyWhiteList.email
|
||||
})
|
||||
.from(resourcePolicyWhiteList)
|
||||
.where(
|
||||
eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId)
|
||||
);
|
||||
|
||||
const policyRules = await db
|
||||
.select({
|
||||
ruleId: resourcePolicyRules.ruleId,
|
||||
enabled: resourcePolicyRules.enabled,
|
||||
priority: resourcePolicyRules.priority,
|
||||
action: resourcePolicyRules.action,
|
||||
match: resourcePolicyRules.match,
|
||||
value: resourcePolicyRules.value
|
||||
})
|
||||
.from(resourcePolicyRules)
|
||||
.where(eq(resourcePolicyRules.resourcePolicyId, res.resourcePolicyId));
|
||||
|
||||
return {
|
||||
...res,
|
||||
roles: policyRoles,
|
||||
users: policyUsers,
|
||||
emailWhiteList: policyEmailWhiteList,
|
||||
rules: policyRules
|
||||
};
|
||||
}
|
||||
|
||||
export type GetResourcePolicyResponse = NonNullable<
|
||||
Awaited<ReturnType<typeof queryResourcePolicy>>
|
||||
>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/resource-policy/{niceId}",
|
||||
description:
|
||||
"Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string(),
|
||||
niceId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Get a resource policy by its resourcePolicyId.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: z.object({
|
||||
resourcePolicyId: z.number()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResourcePolicy(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getResourcePolicySchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const policy = await queryResourcePolicy(parsedParams.data);
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetResourcePolicyResponse>(res, {
|
||||
data: policy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource Policy retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
8
server/routers/policy/index.ts
Normal file
8
server/routers/policy/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from "./getResourcePolicy";
|
||||
export * from "./updateResourcePolicy";
|
||||
export * from "./setResourcePolicyAccessControl";
|
||||
export * from "./setResourcePolicyPassword";
|
||||
export * from "./setResourcePolicyPincode";
|
||||
export * from "./setResourcePolicyHeaderAuth";
|
||||
export * from "./setResourcePolicyWhitelist";
|
||||
export * from "./setResourcePolicyRules";
|
||||
237
server/routers/policy/setResourcePolicyAccessControl.ts
Normal file
237
server/routers/policy/setResourcePolicyAccessControl.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
db,
|
||||
idp,
|
||||
idpOrg,
|
||||
resourcePolicies,
|
||||
rolePolicies,
|
||||
roles,
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { userPolicies } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourcePolicyAcccessControlBodySchema = z.strictObject({
|
||||
sso: z.boolean(),
|
||||
userIds: z.array(z.string()),
|
||||
roleIds: z.array(z.int().positive()).openapi({
|
||||
type: "array"
|
||||
}),
|
||||
skipToIdpId: z.int().positive().optional().nullable().openapi({
|
||||
type: "integer",
|
||||
description: "Page number to retrieve"
|
||||
})
|
||||
});
|
||||
|
||||
const setResourcePolicyAccessControlParamsSchema = z.strictObject({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource-policy/{resourceId}/access-control",
|
||||
description:
|
||||
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
|
||||
tags: [OpenAPITags.Policy, OpenAPITags.User],
|
||||
request: {
|
||||
params: setResourcePolicyAccessControlParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyAcccessControlBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyAccessControl(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = setResourcePolicyAcccessControlBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { userIds, roleIds, sso, skipToIdpId: idpId } = parsedBody.data;
|
||||
|
||||
const parsedParams =
|
||||
setResourcePolicyAccessControlParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||
.limit(1);
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Resource policy not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if Identity provider in `skipToIdpId` exists
|
||||
if (idpId) {
|
||||
const [provider] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
|
||||
.where(
|
||||
and(eq(idp.idpId, idpId), eq(idpOrg.orgId, policy.orgId))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!provider) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Identity provider not found in this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of the roleIds are admin roles
|
||||
const rolesToCheck = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
inArray(roles.roleId, roleIds),
|
||||
eq(roles.orgId, policy.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
const hasAdminRole = rolesToCheck.some((role) => role.isAdmin);
|
||||
|
||||
if (hasAdminRole) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Admin role cannot be assigned to resources"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get all admin role IDs for this org to exclude from deletion
|
||||
const adminRoles = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, policy.orgId)));
|
||||
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
||||
|
||||
const existingUsers = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.innerJoin(userOrgs, eq(userOrgs.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.orgId, policy.orgId),
|
||||
inArray(users.userId, userIds)
|
||||
)
|
||||
);
|
||||
|
||||
const existingRoles = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, policy.orgId),
|
||||
inArray(roles.roleId, roleIds)
|
||||
)
|
||||
);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Update SSO status
|
||||
await trx
|
||||
.update(resourcePolicies)
|
||||
.set({
|
||||
sso,
|
||||
idpId
|
||||
})
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
// Update roles
|
||||
if (adminRoleIds.length > 0) {
|
||||
await trx.delete(rolePolicies).where(
|
||||
and(
|
||||
eq(rolePolicies.resourcePolicyId, resourcePolicyId),
|
||||
ne(rolePolicies.roleId, adminRoleIds[0]) // delete all but the admin role
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await trx
|
||||
.delete(rolePolicies)
|
||||
.where(eq(rolePolicies.resourcePolicyId, resourcePolicyId));
|
||||
}
|
||||
|
||||
const rolesToAdd = existingRoles.map(({ roleId }) => ({
|
||||
roleId,
|
||||
resourcePolicyId
|
||||
}));
|
||||
|
||||
if (rolesToAdd.length > 0) {
|
||||
await trx.insert(rolePolicies).values(rolesToAdd);
|
||||
}
|
||||
|
||||
// Update users
|
||||
await trx
|
||||
.delete(userPolicies)
|
||||
.where(eq(userPolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
const usersToAdd = existingUsers.map(({ user }) => ({
|
||||
userId: user.userId,
|
||||
resourcePolicyId: resourcePolicyId
|
||||
}));
|
||||
|
||||
if (usersToAdd.length > 0) {
|
||||
await trx.insert(userPolicies).values(usersToAdd);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policy succesfully updated",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
117
server/routers/policy/setResourcePolicyHeaderAuth.ts
Normal file
117
server/routers/policy/setResourcePolicyHeaderAuth.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourcePolicyHeaderAuth } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourcePolicyHeaderAuthParamsSchema = z.object({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const setResourcePolicyHeaderAuthBodySchema = z.strictObject({
|
||||
headerAuth: z
|
||||
.object({
|
||||
user: z.string().min(4).max(100),
|
||||
password: z.string().min(4).max(100),
|
||||
extendedCompatibility: z.boolean()
|
||||
})
|
||||
.nullable()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/header-auth",
|
||||
description:
|
||||
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyHeaderAuthBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyHeaderAuth(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setResourcePolicyHeaderAuthParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = setResourcePolicyHeaderAuthBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { headerAuth } = parsedBody.data;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourcePolicyHeaderAuth)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
);
|
||||
|
||||
if (headerAuth !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
);
|
||||
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId,
|
||||
headerAuthHash,
|
||||
extendedCompatibility: headerAuth.extendedCompatibility
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Header Authentication set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
106
server/routers/policy/setResourcePolicyPassword.ts
Normal file
106
server/routers/policy/setResourcePolicyPassword.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourcePolicyPassword } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourcePolicyPasswordParamsSchema = z.object({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const setResourcePolicyPasswordBodySchema = z.strictObject({
|
||||
password: z.string().min(4).max(100).nullable()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/password",
|
||||
description:
|
||||
"Set the password for a resource policy. Setting the password to null will remove it.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: setResourcePolicyPasswordParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPasswordBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyPassword(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setResourcePolicyPasswordParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = setResourcePolicyPasswordBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { password } = parsedBody.data;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourcePolicyPassword)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicyPassword.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
);
|
||||
|
||||
if (password) {
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
await trx
|
||||
.insert(resourcePolicyPassword)
|
||||
.values({ resourcePolicyId, passwordHash });
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policy password set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
106
server/routers/policy/setResourcePolicyPincode.ts
Normal file
106
server/routers/policy/setResourcePolicyPincode.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourcePolicyPincode } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourcePolicyPincodeParamsSchema = z.object({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const setResourcePolicyPincodeBodySchema = z.strictObject({
|
||||
pincode: z
|
||||
.string()
|
||||
.regex(/^\d{6}$/)
|
||||
.or(z.null())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/pincode",
|
||||
description:
|
||||
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: setResourcePolicyPincodeParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyPincodeBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyPincode(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setResourcePolicyPincodeParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = setResourcePolicyPincodeBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { pincode } = parsedBody.data;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourcePolicyPincode)
|
||||
.where(
|
||||
eq(resourcePolicyPincode.resourcePolicyId, resourcePolicyId)
|
||||
);
|
||||
|
||||
if (pincode) {
|
||||
const pincodeHash = await hashPassword(pincode);
|
||||
|
||||
await trx
|
||||
.insert(resourcePolicyPincode)
|
||||
.values({ resourcePolicyId, pincodeHash, digitLength: 6 });
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policy PIN code set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
167
server/routers/policy/setResourcePolicyRules.ts
Normal file
167
server/routers/policy/setResourcePolicyRules.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourcePolicyRules, resourcePolicies } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
isValidCIDR,
|
||||
isValidIP,
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const ruleSchema = z.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||
type: "string",
|
||||
enum: ["ACCEPT", "DROP", "PASS"],
|
||||
description: "rule action"
|
||||
}),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]).openapi({
|
||||
type: "string",
|
||||
enum: ["CIDR", "IP", "PATH"],
|
||||
description: "rule match"
|
||||
}),
|
||||
value: z.string().min(1),
|
||||
priority: z.int().openapi({
|
||||
type: "integer",
|
||||
description: "Rule priority"
|
||||
}),
|
||||
enabled: z.boolean().optional()
|
||||
});
|
||||
|
||||
const setResourcePolicyRulesBodySchema = z.strictObject({
|
||||
applyRules: z.boolean(),
|
||||
rules: z.array(ruleSchema)
|
||||
});
|
||||
|
||||
const setResourcePolicyRulesParamsSchema = z.strictObject({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/rules",
|
||||
description:
|
||||
"Set all rules for a resource policy at once. This will replace all existing rules.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: setResourcePolicyRulesParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyRulesBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyRules(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setResourcePolicyRulesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = setResourcePolicyRulesBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { applyRules, rules } = parsedBody.data;
|
||||
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||
.limit(1);
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||
);
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
} else if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||
);
|
||||
} else if (
|
||||
rule.match === "PATH" &&
|
||||
!isValidUrlGlobPattern(rule.value)
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.update(resourcePolicies)
|
||||
.set({ applyRules })
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
|
||||
);
|
||||
|
||||
if (rules.length > 0) {
|
||||
await trx.insert(resourcePolicyRules).values(
|
||||
rules.map((rule) => ({
|
||||
resourcePolicyId,
|
||||
...rule
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policy rules set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
132
server/routers/policy/setResourcePolicyWhitelist.ts
Normal file
132
server/routers/policy/setResourcePolicyWhitelist.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourcePolicies, resourcePolicyWhiteList } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const setResourcePolicyWhitelistBodySchema = z.strictObject({
|
||||
emailWhitelistEnabled: z.boolean(),
|
||||
emails: z
|
||||
.array(
|
||||
z.email().or(
|
||||
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||
error: "Invalid email address. Wildcard (*) must be the entire local part."
|
||||
})
|
||||
)
|
||||
)
|
||||
.max(50)
|
||||
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||
});
|
||||
|
||||
const setResourcePolicyWhitelistParamsSchema = z.strictObject({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}/whitelist",
|
||||
description:
|
||||
"Set email whitelist for a resource policy. This will replace all existing emails.",
|
||||
tags: [OpenAPITags.Policy],
|
||||
request: {
|
||||
params: setResourcePolicyWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: setResourcePolicyWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function setResourcePolicyWhitelist(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = setResourcePolicyWhitelistBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = setResourcePolicyWhitelistParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { emailWhitelistEnabled, emails } = parsedBody.data;
|
||||
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.update(resourcePolicies)
|
||||
.set({ emailWhitelistEnabled })
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
// delete all whitelist emails
|
||||
await trx
|
||||
.delete(resourcePolicyWhiteList)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicyWhiteList.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
);
|
||||
|
||||
if (emailWhitelistEnabled && emails.length > 0) {
|
||||
await trx.insert(resourcePolicyWhiteList).values(
|
||||
emails.map((email) => ({
|
||||
email,
|
||||
resourcePolicyId
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Whitelist set for resource policy successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
157
server/routers/policy/updateResourcePolicy.ts
Normal file
157
server/routers/policy/updateResourcePolicy.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import z from "zod";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { db, orgs, resourcePolicies, type ResourcePolicy } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
const updateResourcePolicyParamsSchema = z.strictObject({
|
||||
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const updateResourcePolicyBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
niceId: z.string().min(1).max(255).optional()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource-policy/{resourcePolicyId}",
|
||||
description: "Update a resource policy.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: updateResourcePolicyParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: updateResourcePolicyBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateResourcePolicy(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = updateResourcePolicyParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (req.user && req.userOrgRoleIds?.length === 0) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
}
|
||||
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||
.leftJoin(orgs, eq(resourcePolicies.orgId, orgs.orgId));
|
||||
|
||||
const policy = result?.resourcePolicies;
|
||||
const org = result?.orgs;
|
||||
|
||||
if (!policy || !org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource Policy with ID ${resourcePolicyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = updateResourcePolicyBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
if (updateData.niceId) {
|
||||
const [existingPolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicies.niceId, updateData.niceId),
|
||||
eq(resourcePolicies.orgId, policy.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
existingPolicy &&
|
||||
existingPolicy.resourcePolicyId !== policy.resourcePolicyId
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`A resource policy with niceId "${updateData.niceId}" already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPolicy = await db.transaction(async (trx) => {
|
||||
const [updated] = await trx
|
||||
.update(resourcePolicies)
|
||||
.set({
|
||||
...updateData
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
policy.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
if (!updatedPolicy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to update policy"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<ResourcePolicy>(res, {
|
||||
data: updatedPolicy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policy updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
domains,
|
||||
orgDomains,
|
||||
db,
|
||||
loginPage,
|
||||
orgs,
|
||||
Resource,
|
||||
resources,
|
||||
resourcePolicies,
|
||||
roleResources,
|
||||
rolePolicies,
|
||||
roles,
|
||||
userResources
|
||||
userPolicies,
|
||||
userResources,
|
||||
domainNamespaces
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -20,13 +24,18 @@ import logger from "@server/logger";
|
||||
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||
import config from "@server/lib/config";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { getUniqueResourceName } from "@server/db/names";
|
||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
} from "@server/lib/domainUtils";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import {
|
||||
getUniqueResourceName,
|
||||
getUniqueResourcePolicyName
|
||||
} from "@server/db/names";
|
||||
|
||||
const createResourceParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -311,8 +320,46 @@ async function createHttpResource(
|
||||
let resource: Resource | undefined;
|
||||
|
||||
const niceId = await getUniqueResourceName(orgId);
|
||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const adminRole = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (adminRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||
);
|
||||
}
|
||||
|
||||
const [defaultPolicy] = await trx
|
||||
.insert(resourcePolicies)
|
||||
.values({
|
||||
niceId: policyNiceId,
|
||||
orgId,
|
||||
name: `default policy for ${niceId}`,
|
||||
sso: true,
|
||||
scope: "resource"
|
||||
})
|
||||
.returning();
|
||||
|
||||
// make this policy visible by the admin role
|
||||
await trx.insert(rolePolicies).values({
|
||||
roleId: adminRole[0].roleId,
|
||||
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
});
|
||||
|
||||
// make this policy visible by the current user
|
||||
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
|
||||
await trx.insert(userPolicies).values({
|
||||
userId: req.user?.userId!,
|
||||
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
});
|
||||
}
|
||||
|
||||
const newResource = await trx
|
||||
.insert(resources)
|
||||
.values({
|
||||
@@ -328,22 +375,11 @@ async function createHttpResource(
|
||||
stickySession: stickySession,
|
||||
postAuthPath: postAuthPath,
|
||||
wildcard,
|
||||
health: "unknown"
|
||||
health: "unknown",
|
||||
defaultResourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
})
|
||||
.returning();
|
||||
|
||||
const adminRole = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (adminRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||
);
|
||||
}
|
||||
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: adminRole[0].roleId,
|
||||
resourceId: newResource[0].resourceId
|
||||
@@ -369,7 +405,7 @@ async function createHttpResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (build != "oss") {
|
||||
if (build !== "oss") {
|
||||
await createCertificate(domainId, fullDomain, db);
|
||||
}
|
||||
|
||||
@@ -410,22 +446,10 @@ async function createRawResource(
|
||||
let resource: Resource | undefined;
|
||||
|
||||
const niceId = await getUniqueResourceName(orgId);
|
||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const newResource = await trx
|
||||
.insert(resources)
|
||||
.values({
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
http,
|
||||
protocol,
|
||||
proxyPort
|
||||
// enableProxy
|
||||
})
|
||||
.returning();
|
||||
|
||||
const adminRole = await db
|
||||
const adminRole = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
@@ -437,6 +461,44 @@ async function createRawResource(
|
||||
);
|
||||
}
|
||||
|
||||
const [defaultPolicy] = await trx
|
||||
.insert(resourcePolicies)
|
||||
.values({
|
||||
niceId: policyNiceId,
|
||||
orgId,
|
||||
name: `default policy for ${niceId}`,
|
||||
sso: true,
|
||||
scope: "resource"
|
||||
})
|
||||
.returning();
|
||||
|
||||
// make this policy visible by the admin role
|
||||
await trx.insert(rolePolicies).values({
|
||||
roleId: adminRole[0].roleId,
|
||||
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
});
|
||||
|
||||
// make this policy visible by the current user
|
||||
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
|
||||
await trx.insert(userPolicies).values({
|
||||
userId: req.user?.userId!,
|
||||
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
});
|
||||
}
|
||||
|
||||
const newResource = await trx
|
||||
.insert(resources)
|
||||
.values({
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
http,
|
||||
protocol,
|
||||
proxyPort,
|
||||
defaultResourcePolicyId: defaultPolicy.resourcePolicyId
|
||||
})
|
||||
.returning();
|
||||
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: adminRole[0].roleId,
|
||||
resourceId: newResource[0].resourceId
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, targetHealthCheck } from "@server/db";
|
||||
import { newts, resources, sites, targets } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
db,
|
||||
newts,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
targets
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { addPeer } from "../gerbil/peers";
|
||||
import { removeTargets } from "../newt/targets";
|
||||
import { getAllowedIps } from "../target/helpers";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { removeTargets } from "../newt/targets";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const deleteResourceSchema = z.strictObject({
|
||||
@@ -113,6 +118,18 @@ export async function deleteResource(
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete default resource policy
|
||||
if (deletedResource.defaultResourcePolicyId) {
|
||||
await db
|
||||
.delete(resourcePolicies)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
deletedResource.defaultResourcePolicyId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
db,
|
||||
resourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourcePolicies,
|
||||
resourcePolicyHeaderAuth,
|
||||
resourcePolicyPassword,
|
||||
resourcePolicyPincode,
|
||||
resources
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -60,64 +60,53 @@ export async function getResourceAuthInfo(
|
||||
|
||||
const isGuidInteger = /^\d+$/.test(resourceGuid);
|
||||
|
||||
const buildQuery = (whereClause: ReturnType<typeof eq>) =>
|
||||
db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePolicies,
|
||||
or(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
resources.resourcePolicyId
|
||||
),
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
resources.defaultResourcePolicyId
|
||||
)
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyPincode,
|
||||
eq(
|
||||
resourcePolicyPincode.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyPassword,
|
||||
eq(
|
||||
resourcePolicyPassword.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyHeaderAuth,
|
||||
eq(
|
||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(whereClause)
|
||||
.limit(1);
|
||||
|
||||
const [result] =
|
||||
isGuidInteger && build === "saas"
|
||||
? await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(
|
||||
resourceHeaderAuth.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.where(eq(resources.resourceId, Number(resourceGuid)))
|
||||
.limit(1)
|
||||
: await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(
|
||||
resourceHeaderAuth.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.where(eq(resources.resourceGuid, resourceGuid))
|
||||
.limit(1);
|
||||
? await buildQuery(
|
||||
eq(resources.resourceId, Number(resourceGuid))
|
||||
)
|
||||
: await buildQuery(eq(resources.resourceGuid, resourceGuid));
|
||||
|
||||
const resource = result?.resources;
|
||||
if (!resource) {
|
||||
@@ -126,11 +115,10 @@ export async function getResourceAuthInfo(
|
||||
);
|
||||
}
|
||||
|
||||
const pincode = result?.resourcePincode;
|
||||
const password = result?.resourcePassword;
|
||||
const headerAuth = result?.resourceHeaderAuth;
|
||||
const headerAuthExtendedCompatibility =
|
||||
result?.resourceHeaderAuthExtendedCompatibility;
|
||||
const policy = result?.resourcePolicies;
|
||||
const pincode = result?.resourcePolicyPincode;
|
||||
const password = result?.resourcePolicyPassword;
|
||||
const headerAuth = result?.resourcePolicyHeaderAuth;
|
||||
|
||||
const url = resource.fullDomain
|
||||
? `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
||||
@@ -146,13 +134,13 @@ export async function getResourceAuthInfo(
|
||||
pincode: pincode !== null,
|
||||
headerAuth: headerAuth !== null,
|
||||
headerAuthExtendedCompatibility:
|
||||
headerAuthExtendedCompatibility !== null,
|
||||
sso: resource.sso,
|
||||
headerAuth?.extendedCompatibility ?? false,
|
||||
sso: policy?.sso ?? false,
|
||||
blockAccess: resource.blockAccess,
|
||||
url: url ?? "",
|
||||
wildcard: resource.wildcard ?? false,
|
||||
fullDomain: resource.fullDomain,
|
||||
whitelist: resource.emailWhitelistEnabled,
|
||||
whitelist: policy?.emailWhitelistEnabled ?? false,
|
||||
skipToIdpId: resource.skipToIdpId,
|
||||
orgId: resource.orgId,
|
||||
postAuthPath: resource.postAuthPath ?? null
|
||||
|
||||
109
server/routers/resource/getResourcePolicies.ts
Normal file
109
server/routers/resource/getResourcePolicies.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { db, resources } from "@server/db";
|
||||
import {
|
||||
queryResourcePolicy,
|
||||
type GetResourcePolicyResponse
|
||||
} from "@server/routers/policy/getResourcePolicy";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import z from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const getResourcePoliciesParamsSchema = z.strictObject({
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
export type GetResourcePoliciesResponse = {
|
||||
defaultPolicy: GetResourcePolicyResponse;
|
||||
sharedPolicy: GetResourcePolicyResponse | null;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/policies",
|
||||
description: "Get the inline and shared policies associated with a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: getResourcePoliciesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResourcePolicies(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getResourcePoliciesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select({
|
||||
defaultResourcePolicyId: resources.defaultResourcePolicyId,
|
||||
resourcePolicyId: resources.resourcePolicyId
|
||||
})
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.defaultResourcePolicyId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Resource has no default policy"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [defaultPolicy, sharedPolicy] = await Promise.all([
|
||||
queryResourcePolicy({
|
||||
resourcePolicyId: resource.defaultResourcePolicyId
|
||||
}),
|
||||
resource.resourcePolicyId
|
||||
? queryResourcePolicy({
|
||||
resourcePolicyId: resource.resourcePolicyId
|
||||
})
|
||||
: null
|
||||
]);
|
||||
|
||||
return response<GetResourcePoliciesResponse>(res, {
|
||||
data: {
|
||||
defaultPolicy:
|
||||
// the policy will always be non nullable
|
||||
defaultPolicy as unknown as GetResourcePolicyResponse,
|
||||
sharedPolicy
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policies retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -151,8 +151,6 @@ export async function getUserResources(
|
||||
destination: string;
|
||||
mode: string;
|
||||
scheme: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
@@ -166,8 +164,6 @@ export async function getUserResources(
|
||||
destination: siteResources.destination,
|
||||
mode: siteResources.mode,
|
||||
scheme: siteResources.scheme,
|
||||
ssl: siteResources.ssl,
|
||||
fullDomain: siteResources.fullDomain,
|
||||
enabled: siteResources.enabled,
|
||||
alias: siteResources.alias,
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
@@ -255,8 +251,6 @@ export async function getUserResources(
|
||||
destination: siteResource.destination,
|
||||
mode: siteResource.mode,
|
||||
protocol: siteResource.scheme,
|
||||
ssl: siteResource.ssl,
|
||||
fullDomain: siteResource.fullDomain,
|
||||
enabled: siteResource.enabled,
|
||||
alias: siteResource.alias,
|
||||
aliasAddress: siteResource.aliasAddress,
|
||||
@@ -302,8 +296,6 @@ export type GetUserResourcesResponse = {
|
||||
destination: string;
|
||||
mode: string;
|
||||
protocol: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
|
||||
@@ -33,3 +33,4 @@ export * from "./removeUserFromResource";
|
||||
export * from "./listAllResourceNames";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
export * from "./getStatusHistory";
|
||||
export * from "./getResourcePolicies";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
db,
|
||||
resourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourcePolicies,
|
||||
resourcePolicyHeaderAuth,
|
||||
resourcePolicyPassword,
|
||||
resourcePolicyPincode,
|
||||
resources,
|
||||
roleResources,
|
||||
sites,
|
||||
@@ -163,10 +163,10 @@ function queryResourcesBase() {
|
||||
name: resources.name,
|
||||
ssl: resources.ssl,
|
||||
fullDomain: resources.fullDomain,
|
||||
passwordId: resourcePassword.passwordId,
|
||||
sso: resources.sso,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
whitelist: resources.emailWhitelistEnabled,
|
||||
passwordId: resourcePolicyPassword.passwordId,
|
||||
sso: resourcePolicies.sso,
|
||||
pincodeId: resourcePolicyPincode.pincodeId,
|
||||
whitelist: resourcePolicies.emailWhitelistEnabled,
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
@@ -174,29 +174,45 @@ function queryResourcesBase() {
|
||||
domainId: resources.domainId,
|
||||
niceId: resources.niceId,
|
||||
wildcard: resources.wildcard,
|
||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||
headerAuthExtendedCompatibilityId:
|
||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
||||
health: resources.health
|
||||
health: resources.health,
|
||||
headerAuthId: resourcePolicyHeaderAuth.headerAuthId,
|
||||
headerAuthExtendedCompatibility:
|
||||
resourcePolicyHeaderAuth.extendedCompatibility
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
resourcePolicies,
|
||||
or(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
resources.resourcePolicyId
|
||||
),
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
resources.defaultResourcePolicyId
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourcePolicyPassword,
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
resourcePolicyPassword.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyPincode,
|
||||
eq(
|
||||
resourcePolicyPincode.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePolicyHeaderAuth,
|
||||
eq(
|
||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||
resourcePolicies.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||
@@ -206,10 +222,10 @@ function queryResourcesBase() {
|
||||
)
|
||||
.groupBy(
|
||||
resources.resourceId,
|
||||
resourcePassword.passwordId,
|
||||
resourcePincode.pincodeId,
|
||||
resourceHeaderAuth.headerAuthId,
|
||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId
|
||||
resourcePolicies.resourcePolicyId,
|
||||
resourcePolicyPassword.passwordId,
|
||||
resourcePolicyPincode.pincodeId,
|
||||
resourcePolicyHeaderAuth.headerAuthId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -355,21 +371,21 @@ export async function listResources(
|
||||
case "protected":
|
||||
conditions.push(
|
||||
or(
|
||||
eq(resources.sso, true),
|
||||
eq(resources.emailWhitelistEnabled, true),
|
||||
not(isNull(resourceHeaderAuth.headerAuthId)),
|
||||
not(isNull(resourcePincode.pincodeId)),
|
||||
not(isNull(resourcePassword.passwordId))
|
||||
eq(resourcePolicies.sso, true),
|
||||
eq(resourcePolicies.emailWhitelistEnabled, true),
|
||||
not(isNull(resourcePolicyHeaderAuth.headerAuthId)),
|
||||
not(isNull(resourcePolicyPincode.pincodeId)),
|
||||
not(isNull(resourcePolicyPassword.passwordId))
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "not_protected":
|
||||
conditions.push(
|
||||
not(eq(resources.sso, true)),
|
||||
not(eq(resources.emailWhitelistEnabled, true)),
|
||||
isNull(resourceHeaderAuth.headerAuthId),
|
||||
isNull(resourcePincode.pincodeId),
|
||||
isNull(resourcePassword.passwordId)
|
||||
not(eq(resourcePolicies.sso, true)),
|
||||
not(eq(resourcePolicies.emailWhitelistEnabled, true)),
|
||||
isNull(resourcePolicyHeaderAuth.headerAuthId),
|
||||
isNull(resourcePolicyPincode.pincodeId),
|
||||
isNull(resourcePolicyPassword.passwordId)
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -446,9 +462,9 @@ export async function listResources(
|
||||
ssl: row.ssl,
|
||||
fullDomain: row.fullDomain,
|
||||
passwordId: row.passwordId,
|
||||
sso: row.sso,
|
||||
sso: row.sso ?? false,
|
||||
pincodeId: row.pincodeId,
|
||||
whitelist: row.whitelist,
|
||||
whitelist: row.whitelist ?? false,
|
||||
http: row.http,
|
||||
protocol: row.protocol,
|
||||
proxyPort: row.proxyPort,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { Resource, ResourcePolicy } from "@server/db";
|
||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
|
||||
export type GetMaintenanceInfoResponse = {
|
||||
resourceId: number;
|
||||
name: string;
|
||||
@@ -8,3 +11,19 @@ export type GetMaintenanceInfoResponse = {
|
||||
maintenanceMessage: string | null;
|
||||
maintenanceEstimatedTime: string | null;
|
||||
};
|
||||
|
||||
export type AttachedResource = Pick<
|
||||
Resource,
|
||||
"resourceId" | "name" | "fullDomain"
|
||||
>;
|
||||
|
||||
export type ResourcePolicyWithResources = Pick<
|
||||
ResourcePolicy,
|
||||
"resourcePolicyId" | "niceId" | "name" | "orgId"
|
||||
> & {
|
||||
resources: Array<AttachedResource>;
|
||||
};
|
||||
|
||||
export type ListResourcePoliciesResponse = PaginatedResponse<{
|
||||
policies: Array<ResourcePolicyWithResources>;
|
||||
}>;
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||
import {
|
||||
db,
|
||||
domainNamespaces,
|
||||
loginPage,
|
||||
resourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resourceWhitelist
|
||||
} from "@server/db";
|
||||
import {
|
||||
domains,
|
||||
Org,
|
||||
orgDomains,
|
||||
orgs,
|
||||
Resource,
|
||||
resourcePolicies,
|
||||
resources
|
||||
} from "@server/db";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
@@ -24,7 +35,10 @@ import {
|
||||
import { registry } from "@server/openApi";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
} from "@server/lib/domainUtils";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -68,7 +82,8 @@ const updateHttpResourceBodySchema = z
|
||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
||||
postAuthPath: z.string().nullable().optional()
|
||||
postAuthPath: z.string().nullable().optional(),
|
||||
resourcePolicyId: z.number().nullable().optional()
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
error: "At least one field must be provided for update"
|
||||
@@ -165,7 +180,8 @@ const updateRawResourceBodySchema = z
|
||||
stickySession: z.boolean().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
proxyProtocol: z.boolean().optional(),
|
||||
proxyProtocolVersion: z.int().min(1).optional()
|
||||
proxyProtocolVersion: z.int().min(1).optional(),
|
||||
resourcePolicyId: z.number().nullable().optional()
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
error: "At least one field must be provided for update"
|
||||
@@ -301,6 +317,42 @@ async function updateHttpResource(
|
||||
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.wildcardSubdomain
|
||||
);
|
||||
|
||||
if (updateData.resourcePolicyId != null) {
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Resource policies are not supported on your current plan. Please upgrade to access this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existingPolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
updateData.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingPolicy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource policy with ID ${updateData.resourcePolicyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.niceId) {
|
||||
const [existingResource] = await db
|
||||
.select()
|
||||
@@ -326,10 +378,6 @@ async function updateHttpResource(
|
||||
|
||||
// Wildcard subdomains are a paid feature
|
||||
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.wildcardSubdomain
|
||||
);
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -474,10 +522,6 @@ async function updateHttpResource(
|
||||
headers = null;
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.maintencePage
|
||||
);
|
||||
if (!isLicensed) {
|
||||
updateData.maintenanceModeEnabled = undefined;
|
||||
updateData.maintenanceModeType = undefined;
|
||||
@@ -535,38 +579,122 @@ async function updateRawResource(
|
||||
}
|
||||
|
||||
const updateData = parsedBody.data;
|
||||
let updatedResource: Resource | null = null;
|
||||
|
||||
if (updateData.niceId) {
|
||||
const [existingResource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.niceId, updateData.niceId),
|
||||
eq(resources.orgId, resource.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
existingResource &&
|
||||
existingResource.resourceId !== resource.resourceId
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`A resource with niceId "${updateData.niceId}" already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedResource = await db
|
||||
.update(resources)
|
||||
.set(updateData)
|
||||
const [existingResource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resource.resourceId))
|
||||
.returning();
|
||||
.limit(1);
|
||||
|
||||
if (updatedResource.length === 0) {
|
||||
await db.transaction(async (trx) => {
|
||||
if (updateData.resourcePolicyId != null) {
|
||||
const [existingPolicy] = await trx
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
updateData.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingPolicy) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource policy with ID ${updateData.resourcePolicyId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// we are in an inline policy and we need to clear out the old tables
|
||||
await Promise.all([
|
||||
trx
|
||||
.delete(resourcePassword)
|
||||
.where(
|
||||
eq(
|
||||
resourcePassword.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
),
|
||||
trx
|
||||
.delete(resourcePincode)
|
||||
.where(
|
||||
eq(
|
||||
resourcePincode.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
),
|
||||
trx
|
||||
.delete(resourceHeaderAuth)
|
||||
.where(
|
||||
eq(
|
||||
resourceHeaderAuth.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
),
|
||||
trx
|
||||
.delete(resourceHeaderAuthExtendedCompatibility)
|
||||
.where(
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
),
|
||||
trx
|
||||
.delete(resourceWhitelist)
|
||||
.where(
|
||||
eq(
|
||||
resourceWhitelist.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
),
|
||||
|
||||
trx
|
||||
.delete(resourceRules)
|
||||
.where(
|
||||
eq(
|
||||
resourceRules.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
if (updateData.niceId) {
|
||||
const [existingResourceConflict] = await trx
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.niceId, updateData.niceId),
|
||||
eq(resources.orgId, resource.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
existingResourceConflict &&
|
||||
existingResourceConflict.resourceId !== resource.resourceId
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`A resource with niceId "${updateData.niceId}" already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[updatedResource] = await trx
|
||||
.update(resources)
|
||||
.set(updateData)
|
||||
.where(eq(resources.resourceId, resource.resourceId))
|
||||
.returning();
|
||||
});
|
||||
|
||||
if (!updatedResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -576,7 +704,7 @@ async function updateRawResource(
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedResource[0],
|
||||
data: updatedResource,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Non-http Resource updated successfully",
|
||||
|
||||
@@ -135,7 +135,7 @@ const listSitesSchema = z.object({
|
||||
page: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
.min(0)
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(1)
|
||||
.default(1)
|
||||
|
||||
@@ -74,14 +74,16 @@ const createSiteResourceSchema = z
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "host") {
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
if (data.mode == "host") {
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||
@@ -94,12 +96,17 @@ const createSiteResourceSchema = z
|
||||
data.alias.trim() !== "";
|
||||
|
||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||
} else if (data.mode === "http") {
|
||||
// we have to have a domainId defined
|
||||
if (!data.domainId) {
|
||||
return false;
|
||||
}
|
||||
} else if (data.mode === "cidr") {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "cidr") {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
@@ -109,8 +116,7 @@ const createSiteResourceSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
|
||||
@@ -104,17 +104,6 @@ const updateSiteResourceSchema = z
|
||||
data.alias.trim() !== "";
|
||||
|
||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||
} else if (data.mode === "cidr" && data.destination) {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
.safeParse(data.destination).success;
|
||||
return isValidCIDR;
|
||||
} else if (data.mode === "http") {
|
||||
// we have to have a domainId defined
|
||||
if (!data.domainId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -123,6 +112,21 @@ const updateSiteResourceSchema = z
|
||||
"Destination must be a valid IP address or valid domain AND alias is required"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "cidr" && data.destination) {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
.safeParse(data.destination).success;
|
||||
return isValidCIDR;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode !== "http") return true;
|
||||
|
||||
@@ -47,10 +47,7 @@ export async function queryUser(orgId: string, userId: string) {
|
||||
.from(userOrgRoles)
|
||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.userId, userId),
|
||||
eq(userOrgRoles.orgId, orgId)
|
||||
)
|
||||
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||
);
|
||||
|
||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||
@@ -146,7 +143,7 @@ export async function getOrgUser(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
"User does not have permission to get organization user details"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default async function migration() {
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Updated names for ${existingHealthChecks.length} existing targetHealthCheck row(s)`
|
||||
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Error while migrating targetHealthCheck rows:", e);
|
||||
|
||||
@@ -500,7 +500,6 @@ export default function GeneralPage() {
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue("autoProvision", checked);
|
||||
}}
|
||||
orgId={orgId as string}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
|
||||
@@ -246,31 +246,123 @@ export default function Page() {
|
||||
|
||||
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
||||
|
||||
<fieldset
|
||||
disabled={disabled}
|
||||
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
||||
>
|
||||
<SettingsContainer>
|
||||
<fieldset disabled={disabled} className={disabled ? "opacity-50 pointer-events-none" : ""}>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpCreateSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<OidcIdpProviderTypeSelect
|
||||
value={form.watch("type")}
|
||||
onTypeChange={(next) => {
|
||||
applyOidcIdpProviderType(form.setValue, next);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("idpDisplayName")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Auto Provision Settings */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpAutoProvisionUsers")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
<IdpAutoProvisionUsersDescription />
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
autoProvision={
|
||||
form.watch("autoProvision") as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue("autoProvision", checked);
|
||||
}}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
}}
|
||||
roles={roles}
|
||||
fixedRoleNames={fixedRoleNames}
|
||||
onFixedRoleNamesChange={setFixedRoleNames}
|
||||
mappingBuilderClaimPath={
|
||||
mappingBuilderClaimPath
|
||||
}
|
||||
onMappingBuilderClaimPathChange={
|
||||
setMappingBuilderClaimPath
|
||||
}
|
||||
mappingBuilderRules={mappingBuilderRules}
|
||||
onMappingBuilderRulesChange={
|
||||
setMappingBuilderRules
|
||||
}
|
||||
rawExpression={rawRoleExpression}
|
||||
onRawExpressionChange={setRawRoleExpression}
|
||||
orgMappingField={{
|
||||
control: form.control,
|
||||
name: "orgMapping"
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{form.watch("type") === "google" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpTitle")}
|
||||
{t("idpGoogleConfigurationTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpCreateSettingsDescription")}
|
||||
{t("idpGoogleConfigurationDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<OidcIdpProviderTypeSelect
|
||||
value={form.watch("type")}
|
||||
onTypeChange={(next) => {
|
||||
applyOidcIdpProviderType(
|
||||
form.setValue,
|
||||
next
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -280,17 +372,43 @@ export default function Page() {
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("idpDisplayName")}
|
||||
{t(
|
||||
"idpGoogleClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpGoogleClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -301,504 +419,350 @@ export default function Page() {
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Auto Provision Settings */}
|
||||
{form.watch("type") === "azure" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpAutoProvisionUsers")}
|
||||
{t("idpAzureConfigurationTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
<IdpAutoProvisionUsersDescription />
|
||||
{t("idpAzureConfigurationDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
autoProvision={
|
||||
form.watch(
|
||||
"autoProvision"
|
||||
) as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
checked
|
||||
);
|
||||
}}
|
||||
orgId={params.orgId as string}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
}}
|
||||
roles={roles}
|
||||
fixedRoleNames={fixedRoleNames}
|
||||
onFixedRoleNamesChange={
|
||||
setFixedRoleNames
|
||||
}
|
||||
mappingBuilderClaimPath={
|
||||
mappingBuilderClaimPath
|
||||
}
|
||||
onMappingBuilderClaimPathChange={
|
||||
setMappingBuilderClaimPath
|
||||
}
|
||||
mappingBuilderRules={
|
||||
mappingBuilderRules
|
||||
}
|
||||
onMappingBuilderRulesChange={
|
||||
setMappingBuilderRules
|
||||
}
|
||||
rawExpression={rawRoleExpression}
|
||||
onRawExpressionChange={
|
||||
setRawRoleExpression
|
||||
}
|
||||
orgMappingField={{
|
||||
control: form.control,
|
||||
name: "orgMapping"
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tenantId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpTenantIdLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureTenantIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureClientIdDescription2"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureClientSecretDescription2"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{form.watch("type") === "google" && (
|
||||
{form.watch("type") === "oidc" && (
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpGoogleConfigurationTitle")}
|
||||
{t("idpOidcConfigure")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpGoogleConfigurationDescription")}
|
||||
{t("idpOidcConfigureDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpGoogleClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpClientSecret"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpGoogleClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpAuthUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-idp.com/oauth2/authorize"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAuthUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tokenUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpTokenUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-idp.com/oauth2/token"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpTokenUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{form.watch("type") === "azure" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpAzureConfigurationTitle")}
|
||||
{t("idpToken")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpAzureConfigurationDescription")}
|
||||
{t("idpTokenDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="identifierPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpJmespathLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathLabelDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tenantId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpTenantIdLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureTenantIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureClientIdDescription2"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpJmespathEmailPathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathEmailPathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpClientSecret"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAzureClientSecretDescription2"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="namePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpJmespathNamePathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathNamePathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scopes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpOidcConfigureScopes"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpOidcConfigureScopesDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsSectionGrid>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
{form.watch("type") === "oidc" && (
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpOidcConfigure")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpOidcConfigureDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpClientSecret"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpAuthUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-idp.com/oauth2/authorize"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpAuthUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tokenUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("idpTokenUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-idp.com/oauth2/token"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpTokenUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpToken")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpTokenDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="identifierPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpJmespathLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathLabelDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpJmespathEmailPathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathEmailPathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="namePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpJmespathNamePathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpJmespathNamePathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scopes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"idpOidcConfigureScopes"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"idpOidcConfigureScopesDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsSectionGrid>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push(`/${params.orgId}/settings/idp`);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createLoading || disabled}
|
||||
loading={createLoading}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
// log any issues with the form
|
||||
console.log(form.formState.errors);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{t("idpSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push(`/${params.orgId}/settings/idp`);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createLoading || disabled}
|
||||
loading={createLoading}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
// log any issues with the form
|
||||
console.log(form.formState.errors);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{t("idpSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
|
||||
23
src/app/[orgId]/settings/(private)/policies/layout.tsx
Normal file
23
src/app/[orgId]/settings/(private)/policies/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import type { GetOrgResponse } from "@server/routers/org";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export interface PolicyLayoutPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function PolicyLayoutPage(props: PolicyLayoutPageProps) {
|
||||
const params = await props.params;
|
||||
|
||||
let org: GetOrgResponse | null = null;
|
||||
try {
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}/settings`);
|
||||
}
|
||||
|
||||
return <OrgProvider org={org}>{props.children}</OrgProvider>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider";
|
||||
import type { GetResourcePolicyResponse } from "@server/routers/policy";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export interface EditPolicyPageProps {
|
||||
params: Promise<{ niceId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function EditPolicyPage(props: EditPolicyPageProps) {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
|
||||
let policyResponse: GetResourcePolicyResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<GetResourcePolicyResponse>
|
||||
>(
|
||||
`/org/${params.orgId}/resource-policy/${params.niceId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
policyResponse = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}/settings/policies/resource`);
|
||||
}
|
||||
|
||||
if (!policyResponse) {
|
||||
redirect(`/${params.orgId}/settings/policies/resource`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={t("resourcePolicySetting", {
|
||||
policyName: policyResponse.name
|
||||
})}
|
||||
description={t("resourcePolicySettingDescription")}
|
||||
/>
|
||||
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/${params.orgId}/settings/policies/resource`}>
|
||||
{t("resourcePoliciesSeeAll")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ResourcePolicyProvider policy={policyResponse}>
|
||||
<EditPolicyForm />
|
||||
</ResourcePolicyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CreatePolicyForm } from "@app/components/resource-policy/CreatePolicyForm";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
|
||||
export interface CreateResourcePolicyPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function CreateResourcePolicyPage(
|
||||
props: CreateResourcePolicyPageProps
|
||||
) {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={t("resourcePoliciesCreate")}
|
||||
description={t("resourcePoliciesCreateDescription")}
|
||||
/>
|
||||
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/${params.orgId}/settings/policies/resource`}>
|
||||
{t("resourcePoliciesSeeAll")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CreatePolicyForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ResourcePoliciesTable } from "@app/components/ResourcePoliciesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import type { GetOrgResponse } from "@server/routers/org";
|
||||
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export interface ResourcePoliciesPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}
|
||||
|
||||
export default async function ResourcePoliciesPage(
|
||||
props: ResourcePoliciesPageProps
|
||||
) {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const searchParams = new URLSearchParams(await props.searchParams);
|
||||
|
||||
let org: GetOrgResponse | null = null;
|
||||
try {
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}/settings/resources`);
|
||||
}
|
||||
|
||||
let policies: ListResourcePoliciesResponse["policies"] = [];
|
||||
let pagination: ListResourcePoliciesResponse["pagination"] = {
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
};
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<ListResourcePoliciesResponse>
|
||||
>(
|
||||
`/org/${params.orgId}/resource-policies?${searchParams.toString()}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
const responseData = res.data.data;
|
||||
policies = responseData.policies;
|
||||
pagination = responseData.pagination;
|
||||
} catch (e) {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("resourcePoliciesTitle")}
|
||||
description={t("resourcePoliciesDescription")}
|
||||
/>
|
||||
|
||||
<ResourcePoliciesTable
|
||||
policies={policies}
|
||||
orgId={params.orgId}
|
||||
rowCount={pagination.total}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.pageSize
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,10 @@
|
||||
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function NewAlertRulePage() {
|
||||
const params = useParams();
|
||||
@@ -16,19 +14,6 @@ export default function NewAlertRulePage() {
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||
const { env } = useEnvContext();
|
||||
const router = useRouter();
|
||||
const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures;
|
||||
|
||||
useEffect(() => {
|
||||
if (disableEnterpriseFeatures) {
|
||||
router.replace(`/${orgId}/settings/alerting/rules`);
|
||||
}
|
||||
}, [disableEnterpriseFeatures, orgId, router]);
|
||||
|
||||
if (disableEnterpriseFeatures) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Layout } from "@app/components/Layout";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { orgNavSections } from "@app/app/navigation";
|
||||
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -48,13 +49,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const t = await getTranslations();
|
||||
|
||||
try {
|
||||
const getOrgUser = cache(() =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${params.orgId}/user/${user.userId}`,
|
||||
cookie
|
||||
)
|
||||
);
|
||||
const orgUser = await getOrgUser();
|
||||
const orgUser = await getCachedOrgUser(params.orgId, user.userId);
|
||||
|
||||
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
|
||||
throw new Error(t("userErrorNotAdminOrOwner"));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,10 +96,10 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
title: t("authentication"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
|
||||
});
|
||||
navItems.push({
|
||||
title: t("rules"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
|
||||
});
|
||||
// navItems.push({
|
||||
// title: t("rules"),
|
||||
// href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
|
||||
// });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -92,7 +92,13 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { toASCII } from "punycode";
|
||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
useTransition,
|
||||
useEffect
|
||||
} from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -218,7 +224,7 @@ export default function Page() {
|
||||
>([]);
|
||||
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
|
||||
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [createLoading, startTransition] = useTransition();
|
||||
const [showSnippets, setShowSnippets] = useState(false);
|
||||
const [niceId, setNiceId] = useState<string>("");
|
||||
|
||||
@@ -328,7 +334,7 @@ export default function Page() {
|
||||
id: "raw" as ResourceType,
|
||||
title: t("resourceRaw"),
|
||||
description:
|
||||
build == "saas"
|
||||
build === "saas"
|
||||
? t("resourceRawDescriptionCloud")
|
||||
: t("resourceRawDescription")
|
||||
}
|
||||
@@ -473,8 +479,6 @@ export default function Page() {
|
||||
);
|
||||
|
||||
async function onSubmit() {
|
||||
setCreateLoading(true);
|
||||
|
||||
const baseData = baseForm.getValues();
|
||||
const isHttp = baseData.http;
|
||||
|
||||
@@ -610,8 +614,6 @@ export default function Page() {
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
setCreateLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1465,7 +1467,7 @@ export default function Page() {
|
||||
console.log(httpForm.getValues());
|
||||
|
||||
if (baseValid && settingsValid) {
|
||||
onSubmit();
|
||||
startTransition(onSubmit);
|
||||
}
|
||||
}}
|
||||
loading={createLoading}
|
||||
|
||||
@@ -681,9 +681,6 @@ export default function PoliciesPage() {
|
||||
control: form.control,
|
||||
name: "orgMapping"
|
||||
}}
|
||||
orgId={
|
||||
editingPolicy?.orgId || policyFormOrgId
|
||||
}
|
||||
roleMappingFieldIdPrefix="admin-idp-policy-role"
|
||||
roleMappingMode={policyRoleMappingMode}
|
||||
onRoleMappingModeChange={
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CreditCard,
|
||||
Fingerprint,
|
||||
Globe,
|
||||
GlobeIcon,
|
||||
GlobeLock,
|
||||
KeyRound,
|
||||
Laptop,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
ScanEye,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldIcon,
|
||||
SquareMousePointer,
|
||||
TicketCheck,
|
||||
Unplug,
|
||||
@@ -99,7 +101,7 @@ export const orgNavSections = (
|
||||
href: "/{orgId}/settings/domains",
|
||||
icon: <Globe className="size-4 flex-none" />
|
||||
},
|
||||
...(build == "saas"
|
||||
...(build === "saas"
|
||||
? [
|
||||
{
|
||||
title: "sidebarRemoteExitNodes",
|
||||
@@ -134,6 +136,24 @@ export const orgNavSections = (
|
||||
}
|
||||
]
|
||||
},
|
||||
...(build !== "oss"
|
||||
? [
|
||||
{
|
||||
title: "sidebarPolicies",
|
||||
|
||||
icon: <ShieldIcon className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarResourcePolicies",
|
||||
href: "/{orgId}/settings/policies/resource",
|
||||
icon: (
|
||||
<GlobeIcon className="size-4 flex-none" />
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
// PaidFeaturesAlert
|
||||
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
|
||||
build === "saas" ||
|
||||
@@ -212,22 +232,16 @@ export const orgNavSections = (
|
||||
title: "sidebarManagement",
|
||||
icon: <Building2 className="size-4 flex-none" />,
|
||||
items: [
|
||||
...(!env?.flags.disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
title: "sidebarAlerting",
|
||||
href: "/{orgId}/settings/alerting",
|
||||
icon: (
|
||||
<BellRing className="size-4 flex-none" />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "sidebarProvisioning",
|
||||
href: "/{orgId}/settings/provisioning",
|
||||
icon: <Boxes className="size-4 flex-none" />
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "sidebarAlerting",
|
||||
href: "/{orgId}/settings/alerting",
|
||||
icon: <BellRing className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarProvisioning",
|
||||
href: "/{orgId}/settings/provisioning",
|
||||
icon: <Boxes className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarBluePrints",
|
||||
href: "/{orgId}/settings/blueprints",
|
||||
|
||||
@@ -134,9 +134,7 @@ export default function AlertingRulesTable({
|
||||
}: AlertingRulesTableProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const envContext = useEnvContext();
|
||||
const api = createApiClient(envContext);
|
||||
const { env } = envContext;
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isRefreshing, startRefresh] = useTransition();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||
@@ -428,15 +426,9 @@ export default function AlertingRulesTable({
|
||||
searchQuery={query}
|
||||
manualFiltering
|
||||
manualSorting
|
||||
onAdd={
|
||||
!env.flags.disableEnterpriseFeatures
|
||||
? () => {
|
||||
router.push(
|
||||
`/${orgId}/settings/alerting/create`
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onAdd={() => {
|
||||
router.push(`/${orgId}/settings/alerting/create`);
|
||||
}}
|
||||
onRefresh={refreshList}
|
||||
isRefreshing={isRefreshing || isFiltering}
|
||||
addButtonText={t("alertingAddRule")}
|
||||
|
||||
@@ -28,15 +28,14 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { build } from "@server/build";
|
||||
import { validateLocalPath } from "@app/lib/validateLocalPath";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { validateLocalPath } from "@app/lib/validateLocalPath";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export type AuthPageCustomizationProps = {
|
||||
orgId: string;
|
||||
|
||||
@@ -47,7 +47,6 @@ type AutoProvisionConfigWidgetProps = {
|
||||
roleMappingFieldIdPrefix?: string;
|
||||
showFreeformRoleNamesHint?: boolean;
|
||||
autoProvisionSwitchId?: string;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export default function AutoProvisionConfigWidget({
|
||||
@@ -68,8 +67,7 @@ export default function AutoProvisionConfigWidget({
|
||||
showAutoProvisionSwitch = true,
|
||||
roleMappingFieldIdPrefix = "org-idp-auto-provision",
|
||||
showFreeformRoleNamesHint = false,
|
||||
autoProvisionSwitchId = "auto-provision-toggle",
|
||||
orgId
|
||||
autoProvisionSwitchId = "auto-provision-toggle"
|
||||
}: AutoProvisionConfigWidgetProps) {
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
@@ -108,7 +106,6 @@ export default function AutoProvisionConfigWidget({
|
||||
showFreeformRoleNamesHint={
|
||||
showFreeformRoleNamesHint
|
||||
}
|
||||
orgId={orgId}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={onRoleMappingModeChange}
|
||||
roles={roles}
|
||||
|
||||
@@ -840,16 +840,12 @@ export function InternalResourceForm({
|
||||
modeCidrKey
|
||||
)
|
||||
},
|
||||
...(!disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
value: "http" as const,
|
||||
label: t(
|
||||
modeHttpKey
|
||||
)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
{
|
||||
value: "http",
|
||||
label: t(
|
||||
modeHttpKey
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -1129,6 +1125,30 @@ export function InternalResourceForm({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ssl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="internal-resource-ssl"
|
||||
label={t(enableSslLabelKey)}
|
||||
description={t(
|
||||
enableSslDescriptionKey
|
||||
)}
|
||||
checked={!!field.value}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
disabled={
|
||||
httpSectionDisabled
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -129,7 +129,9 @@ export function LayoutSidebar({
|
||||
user.serverAdmin || Boolean(currentOrg?.isOwner || currentOrg?.isAdmin);
|
||||
|
||||
const showTrial =
|
||||
build === "saas" && Boolean(orgId) && subscriptionContext?.isTrial;
|
||||
build === "saas" &&
|
||||
Boolean(orgId) &&
|
||||
subscriptionContext?.isTrial;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -238,16 +240,11 @@ export function LayoutSidebar({
|
||||
<div className="px-4">
|
||||
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-0.2"></div>
|
||||
)}
|
||||
) : <div className="mt-0.2"></div>}
|
||||
|
||||
{showTrial && (
|
||||
<div className="px-4">
|
||||
<ShowTrialCard
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
isOwner={Boolean(currentOrg?.isOwner)}
|
||||
/>
|
||||
<ShowTrialCard isCollapsed={isSidebarCollapsed} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from "@/components/ui/tooltip";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
|
||||
// Update Resource type to include site information
|
||||
type Resource = {
|
||||
@@ -65,8 +64,6 @@ type SiteResource = {
|
||||
destination: string;
|
||||
mode: string;
|
||||
protocol: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
@@ -126,7 +123,6 @@ const ResourceFavicon = ({
|
||||
|
||||
// Resource Info component
|
||||
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
const t = useTranslations();
|
||||
const hasAuthMethods =
|
||||
resource.sso ||
|
||||
resource.password ||
|
||||
@@ -145,9 +141,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
{/* Site Information */}
|
||||
{resource.siteName && (
|
||||
<div>
|
||||
<div className="text-xs font-medium mb-1.5">
|
||||
{t("site")}
|
||||
</div>
|
||||
<div className="text-xs font-medium mb-1.5">Site</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
||||
<span className="text-sm">{resource.siteName}</span>
|
||||
@@ -163,7 +157,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
}
|
||||
>
|
||||
<div className="text-xs font-medium mb-1.5">
|
||||
{t("memberPortalAuthMethods")}
|
||||
Authentication Methods
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{resource.sso && (
|
||||
@@ -172,7 +166,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{t("memberPortalSso")}
|
||||
Single Sign-On (SSO)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -182,7 +176,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{t("memberPortalPasswordProtected")}
|
||||
Password Protected
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -191,9 +185,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
||||
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{t("memberPortalPinCode")}
|
||||
</span>
|
||||
<span className="text-sm">PIN Code</span>
|
||||
</div>
|
||||
)}
|
||||
{resource.whitelist && (
|
||||
@@ -201,9 +193,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
||||
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{t("memberPortalEmailWhitelist")}
|
||||
</span>
|
||||
<span className="text-sm">Email Whitelist</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -218,7 +208,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||
<span className="text-sm text-destructive">
|
||||
{t("memberPortalResourceDisabled")}
|
||||
Resource Disabled
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +233,6 @@ const PaginationControls = ({
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
}) => {
|
||||
const t = useTranslations();
|
||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
@@ -252,11 +241,7 @@ const PaginationControls = ({
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("memberPortalShowingResources", {
|
||||
start: startItem,
|
||||
end: endItem,
|
||||
total: totalItems
|
||||
})}
|
||||
Showing {startItem}-{endItem} of {totalItems} resources
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -268,7 +253,7 @@ const PaginationControls = ({
|
||||
className="gap-1"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
{t("memberPortalPrevious")}
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -324,7 +309,7 @@ const PaginationControls = ({
|
||||
disabled={currentPage === totalPages}
|
||||
className="gap-1"
|
||||
>
|
||||
{t("memberPortalNext")}
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -404,11 +389,13 @@ export default function MemberResourcesPortal({
|
||||
response.data.data.siteResources || []
|
||||
);
|
||||
} else {
|
||||
setError(t("memberPortalFailedToLoad"));
|
||||
setError("Failed to load resources");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching user resources:", err);
|
||||
setError(t("memberPortalFailedToLoadDescription"));
|
||||
setError(
|
||||
"Failed to load resources. Please check your connection and try again."
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -539,8 +526,8 @@ export default function MemberResourcesPortal({
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
<SettingsSectionTitle
|
||||
title={t("memberPortalTitle")}
|
||||
description={t("memberPortalDescription")}
|
||||
title="Resources"
|
||||
description="Resources you have access to in this organization"
|
||||
/>
|
||||
|
||||
{/* Search and Sort Controls - Skeleton */}
|
||||
@@ -567,8 +554,8 @@ export default function MemberResourcesPortal({
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
<SettingsSectionTitle
|
||||
title={t("memberPortalTitle")}
|
||||
description={t("memberPortalDescription")}
|
||||
title="Resources"
|
||||
description="Resources you have access to in this organization"
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||
@@ -576,7 +563,7 @@ export default function MemberResourcesPortal({
|
||||
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||
{t("memberPortalUnableToLoad")}
|
||||
Unable to Load Resources
|
||||
</h3>
|
||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||
{error}
|
||||
@@ -587,7 +574,7 @@ export default function MemberResourcesPortal({
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
{t("memberPortalTryAgain")}
|
||||
Try Again
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -598,8 +585,8 @@ export default function MemberResourcesPortal({
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
<SettingsSectionTitle
|
||||
title={t("memberPortalTitle")}
|
||||
description={t("memberPortalDescription")}
|
||||
title="Resources"
|
||||
description="Resources you have access to in this organization"
|
||||
/>
|
||||
|
||||
{/* Search and Sort Controls with Refresh */}
|
||||
@@ -608,7 +595,7 @@ export default function MemberResourcesPortal({
|
||||
{/* Search */}
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Input
|
||||
placeholder={t("resourcesSearch")}
|
||||
placeholder="Search resources..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-8 bg-card"
|
||||
@@ -620,28 +607,26 @@ export default function MemberResourcesPortal({
|
||||
<div className="w-full sm:w-36">
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="bg-card">
|
||||
<SelectValue
|
||||
placeholder={t("memberPortalSortBy")}
|
||||
/>
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name-asc">
|
||||
{t("memberPortalSortNameAsc")}
|
||||
Name A-Z
|
||||
</SelectItem>
|
||||
<SelectItem value="name-desc">
|
||||
{t("memberPortalSortNameDesc")}
|
||||
Name Z-A
|
||||
</SelectItem>
|
||||
<SelectItem value="domain-asc">
|
||||
{t("memberPortalSortDomainAsc")}
|
||||
Domain A-Z
|
||||
</SelectItem>
|
||||
<SelectItem value="domain-desc">
|
||||
{t("memberPortalSortDomainDesc")}
|
||||
Domain Z-A
|
||||
</SelectItem>
|
||||
<SelectItem value="status-enabled">
|
||||
{t("memberPortalSortEnabledFirst")}
|
||||
Enabled First
|
||||
</SelectItem>
|
||||
<SelectItem value="status-disabled">
|
||||
{t("memberPortalSortDisabledFirst")}
|
||||
Disabled First
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -659,7 +644,7 @@ export default function MemberResourcesPortal({
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("memberPortalRefresh")}
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -678,15 +663,13 @@ export default function MemberResourcesPortal({
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
||||
{searchQuery
|
||||
? t("memberPortalNoResourcesFound")
|
||||
: t("memberPortalNoResourcesAvailable")}
|
||||
? "No Resources Found"
|
||||
: "No Resources Available"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||
{searchQuery
|
||||
? t("memberPortalNoResourcesMatchSearch", {
|
||||
query: searchQuery
|
||||
})
|
||||
: t("memberPortalNoResourcesAccess")}
|
||||
? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.`
|
||||
: "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
{searchQuery ? (
|
||||
@@ -695,7 +678,7 @@ export default function MemberResourcesPortal({
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
{t("memberPortalClearSearch")}
|
||||
Clear Search
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -707,7 +690,7 @@ export default function MemberResourcesPortal({
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("memberPortalRefreshResources")}
|
||||
Refresh Resources
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -721,12 +704,11 @@ export default function MemberResourcesPortal({
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Globe className="h-5 w-5" />
|
||||
{t("memberPortalPublicResources")}
|
||||
Public Resources
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t(
|
||||
"memberPortalPublicResourcesDescription"
|
||||
)}
|
||||
Web applications and services accessible via
|
||||
browser
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||
@@ -786,12 +768,9 @@ export default function MemberResourcesPortal({
|
||||
resource.domain
|
||||
);
|
||||
toast({
|
||||
title: t(
|
||||
"memberPortalCopiedToClipboard"
|
||||
),
|
||||
description: t(
|
||||
"memberPortalCopiedUrlDescription"
|
||||
),
|
||||
title: "Copied to clipboard",
|
||||
description:
|
||||
"Resource URL has been copied to your clipboard.",
|
||||
duration: 2000
|
||||
});
|
||||
}}
|
||||
@@ -812,7 +791,7 @@ export default function MemberResourcesPortal({
|
||||
disabled={!resource.enabled}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||
{t("memberPortalOpenResource")}
|
||||
Open Resource
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -827,12 +806,11 @@ export default function MemberResourcesPortal({
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Combine className="h-5 w-5" />
|
||||
{t("memberPortalPrivateResources")}
|
||||
Private Resources
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t(
|
||||
"memberPortalPrivateResourcesDescription"
|
||||
)}
|
||||
Internal network resources accessible via
|
||||
client
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||
@@ -865,16 +843,11 @@ export default function MemberResourcesPortal({
|
||||
<InfoPopup>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="text-xs font-medium mb-1.5">
|
||||
{t(
|
||||
"memberPortalResourceDetails"
|
||||
)}
|
||||
Resource Details
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"memberPortalMode"
|
||||
)}
|
||||
:
|
||||
Mode:
|
||||
</span>
|
||||
<span className="ml-2 text-muted-foreground capitalize">
|
||||
{
|
||||
@@ -885,10 +858,7 @@ export default function MemberResourcesPortal({
|
||||
{siteResource.protocol && (
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"protocol"
|
||||
)}
|
||||
:
|
||||
Protocol:
|
||||
</span>
|
||||
<span className="ml-2 text-muted-foreground uppercase">
|
||||
{
|
||||
@@ -899,10 +869,7 @@ export default function MemberResourcesPortal({
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"memberPortalDestination"
|
||||
)}
|
||||
:
|
||||
Destination:
|
||||
</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{
|
||||
@@ -913,10 +880,7 @@ export default function MemberResourcesPortal({
|
||||
{siteResource.alias && (
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"memberPortalAlias"
|
||||
)}
|
||||
:
|
||||
Alias:
|
||||
</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{
|
||||
@@ -927,21 +891,14 @@ export default function MemberResourcesPortal({
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"status"
|
||||
)}
|
||||
:
|
||||
Status:
|
||||
</span>
|
||||
<span
|
||||
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{siteResource.enabled
|
||||
? t(
|
||||
"enabled"
|
||||
)
|
||||
: t(
|
||||
"disabled"
|
||||
)}
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -950,14 +907,7 @@ export default function MemberResourcesPortal({
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{siteResource.mode === "http" &&
|
||||
siteResource.fullDomain ? (
|
||||
/* HTTP mode - show as clickable link */
|
||||
<CopyToClipboard
|
||||
text={`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`}
|
||||
isLink={true}
|
||||
/>
|
||||
) : siteResource.alias ? (
|
||||
{siteResource.alias ? (
|
||||
<>
|
||||
{/* Alias as primary */}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -975,13 +925,9 @@ export default function MemberResourcesPortal({
|
||||
siteResource.alias!
|
||||
);
|
||||
toast({
|
||||
title: t(
|
||||
"memberPortalCopiedToClipboard"
|
||||
),
|
||||
title: "Copied to clipboard",
|
||||
description:
|
||||
t(
|
||||
"memberPortalCopiedAliasDescription"
|
||||
),
|
||||
"Resource alias has been copied to your clipboard.",
|
||||
duration: 2000
|
||||
});
|
||||
}}
|
||||
@@ -1013,13 +959,9 @@ export default function MemberResourcesPortal({
|
||||
siteResource.destination
|
||||
);
|
||||
toast({
|
||||
title: t(
|
||||
"memberPortalCopiedToClipboard"
|
||||
),
|
||||
title: "Copied to clipboard",
|
||||
description:
|
||||
t(
|
||||
"memberPortalCopiedDestinationDescription"
|
||||
),
|
||||
"Resource destination has been copied to your clipboard.",
|
||||
duration: 2000
|
||||
});
|
||||
}}
|
||||
@@ -1031,34 +973,10 @@ export default function MemberResourcesPortal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 pt-0 mt-auto space-y-2">
|
||||
{siteResource.mode === "http" &&
|
||||
siteResource.fullDomain ? (
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`,
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
className="w-full h-9"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={
|
||||
!siteResource.enabled
|
||||
}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||
{t(
|
||||
"memberPortalOpenResource"
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="p-6 pt-0 mt-auto">
|
||||
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
|
||||
<Combine className="h-3.5 w-3.5 mr-2" />
|
||||
{t(
|
||||
"memberPortalRequiresClientConnection"
|
||||
)}
|
||||
Requires Client Connection
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -193,22 +193,17 @@ export default function ProxyResourcesTable({
|
||||
});
|
||||
};
|
||||
|
||||
const deleteResource = (resourceId: number) => {
|
||||
api.delete(`/resource/${resourceId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("resourceErrorDelte"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorDelte"),
|
||||
description: formatAxiosError(e, t("resourceErrorDelte"))
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
});
|
||||
const deleteResource = async (resourceId: number) => {
|
||||
await api.delete(`/resource/${resourceId}`).catch((e) => {
|
||||
console.error(t("resourceErrorDelte"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorDelte"),
|
||||
description: formatAxiosError(e, t("resourceErrorDelte"))
|
||||
});
|
||||
});
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
};
|
||||
|
||||
async function toggleResourceEnabled(val: boolean, resourceId: number) {
|
||||
@@ -770,7 +765,11 @@ export default function ProxyResourcesTable({
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
onConfirm={async () => deleteResource(selectedResource!.id)}
|
||||
onConfirm={async () =>
|
||||
startTransition(() =>
|
||||
deleteResource(selectedResource!.id)
|
||||
)
|
||||
}
|
||||
string={selectedResource.name}
|
||||
title={t("resourceDelete")}
|
||||
/>
|
||||
|
||||
311
src/components/ResourcePoliciesTable.tsx
Normal file
311
src/components/ResourcePoliciesTable.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
"use client";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import type {
|
||||
AttachedResource,
|
||||
ListResourcePoliciesResponse
|
||||
} from "@server/routers/resource/types";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronDown,
|
||||
MoreHorizontal,
|
||||
Waypoints
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { Button } from "./ui/button";
|
||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||
import type { ExtendedColumnDef } from "./ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "./ui/dropdown-menu";
|
||||
import ConfirmDeleteDialog from "./ConfirmDeleteDialog";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number];
|
||||
|
||||
export type ResourcePoliciesTableProps = {
|
||||
policies: Array<ResourcePolicyRow>;
|
||||
orgId: string;
|
||||
pagination: PaginationState;
|
||||
rowCount: number;
|
||||
};
|
||||
|
||||
export function ResourcePoliciesTable({
|
||||
policies,
|
||||
orgId,
|
||||
pagination,
|
||||
rowCount
|
||||
}: ResourcePoliciesTableProps) {
|
||||
const router = useRouter();
|
||||
const {
|
||||
navigate: filter,
|
||||
isNavigating: isFiltering,
|
||||
searchParams
|
||||
} = useNavigationContext();
|
||||
const t = useTranslations();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedResourcePolicy, setSelectedResourcePolicy] =
|
||||
useState<ResourcePolicyRow | null>(null);
|
||||
|
||||
const deleteResourcePolicy = async (resourcePolicyId: number) => {
|
||||
await api
|
||||
.delete(`/resource-policy/${resourcePolicyId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("resourceErrorDelte"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorDelte"),
|
||||
description: formatAxiosError(e, t("resourceErrorDelte"))
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||
|
||||
const refreshData = () => {
|
||||
startTransition(() => {
|
||||
try {
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function ResourceListCell({
|
||||
resources
|
||||
}: {
|
||||
resources?: AttachedResource[];
|
||||
}) {
|
||||
if (!resources || resources.length === 0) {
|
||||
return (
|
||||
<div
|
||||
id="LOOK_FOR_ME"
|
||||
className="flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Waypoints className="size-4 flex-none" />
|
||||
<span className="text-sm">
|
||||
{t("resourcePoliciesAttachedResourcesEmpty")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 h-8 px-0 font-normal"
|
||||
>
|
||||
<Waypoints className="size-4 flex-none" />
|
||||
<span className="text-sm">
|
||||
{t("resourcePoliciesAttachedResources", {
|
||||
count: resources.length
|
||||
})}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-70">
|
||||
{resources.map((resource) => (
|
||||
<DropdownMenuItem
|
||||
key={resource.resourceId}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{resource.name}
|
||||
</div>
|
||||
<span
|
||||
className={`capitalize text-muted-foreground`}
|
||||
>
|
||||
{resource.fullDomain}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const proxyColumns: ExtendedColumnDef<ResourcePolicyRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
enableHiding: false,
|
||||
friendlyName: t("name"),
|
||||
header: () => <span className="p-3">{t("name")}</span>,
|
||||
cell: ({ row }) => <span>{row.original.name}</span>
|
||||
},
|
||||
{
|
||||
id: "niceId",
|
||||
accessorKey: "nice",
|
||||
friendlyName: t("identifier"),
|
||||
enableHiding: true,
|
||||
header: () => <span className="p-3">{t("identifier")}</span>,
|
||||
cell: ({ row }) => {
|
||||
return <span>{row.original.niceId || "-"}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "resources",
|
||||
accessorKey: "resources",
|
||||
friendlyName: t("resourcePoliciesAttachedResourcesColumnTitle"),
|
||||
header: () => (
|
||||
<span className="p-3">
|
||||
{t("resourcePoliciesAttachedResourcesColumnTitle")}
|
||||
</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return <ResourceListCell resources={row.original.resources} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const policyRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${policyRow.orgId}/settings/policies/resource/${policyRow.niceId}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedResourcePolicy(policyRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${policyRow.orgId}/settings/policies/resource/${policyRow.niceId}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const handlePaginationChange = (newPage: PaginationState) => {
|
||||
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
||||
searchParams.set("pageSize", newPage.pageSize.toString());
|
||||
filter({
|
||||
searchParams
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearchChange = useDebouncedCallback((query: string) => {
|
||||
searchParams.set("query", query);
|
||||
searchParams.delete("page");
|
||||
filter({
|
||||
searchParams
|
||||
});
|
||||
}, 300);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix[TierFeature.ResourcePolicies]}
|
||||
/>
|
||||
{selectedResourcePolicy && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedResourcePolicy(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("resourcePolicyQuestionRemove")}</p>
|
||||
<p>{t("resourcePolicyMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourcePolicyDeleteConfirm")}
|
||||
onConfirm={async () =>
|
||||
deleteResourcePolicy(
|
||||
selectedResourcePolicy.resourcePolicyId
|
||||
)
|
||||
}
|
||||
string={selectedResourcePolicy.name}
|
||||
title={t("resourcePolicyDelete")}
|
||||
/>
|
||||
)}
|
||||
<ControlledDataTable
|
||||
columns={proxyColumns}
|
||||
rows={policies}
|
||||
tableId="resource-policies"
|
||||
searchPlaceholder={t("resourcePoliciesSearch")}
|
||||
pagination={pagination}
|
||||
rowCount={rowCount}
|
||||
onSearch={handleSearchChange}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
onAdd={() =>
|
||||
startNavigation(() =>
|
||||
router.push(
|
||||
`/${orgId}/settings/policies/resource/create`
|
||||
)
|
||||
)
|
||||
}
|
||||
addButtonText={t("resourcePoliciesAdd")}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing || isFiltering}
|
||||
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||
enableColumnVisibility
|
||||
columnVisibility={{ niceId: false }}
|
||||
stickyLeftColumn="name"
|
||||
stickyRightColumn="actions"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { build } from "@server/build";
|
||||
import { RolesSelector } from "./roles-selector";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
export type RoleMappingRoleOption = {
|
||||
roleId: number;
|
||||
@@ -39,8 +40,6 @@ export type RoleMappingConfigFieldsProps = {
|
||||
fieldIdPrefix?: string;
|
||||
/** When true, show extra hint for global default policies (no org role list). */
|
||||
showFreeformRoleNamesHint?: boolean;
|
||||
/** Org ID to use for role lookup. Falls back to URL params when not provided. */
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export default function RoleMappingConfigFields({
|
||||
@@ -56,13 +55,14 @@ export default function RoleMappingConfigFields({
|
||||
rawExpression,
|
||||
onRawExpressionChange,
|
||||
fieldIdPrefix = "role-mapping",
|
||||
showFreeformRoleNamesHint = false,
|
||||
orgId
|
||||
showFreeformRoleNamesHint = false
|
||||
}: RoleMappingConfigFieldsProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const { orgId } = useParams();
|
||||
|
||||
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
||||
const showSingleRoleDisclaimer =
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
@@ -95,10 +95,6 @@ export default function RoleMappingConfigFields({
|
||||
}
|
||||
}, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]);
|
||||
|
||||
const [fixedRolesActiveTagIndex, setFixedRolesActiveTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
||||
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
||||
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
||||
@@ -165,94 +161,38 @@ export default function RoleMappingConfigFields({
|
||||
|
||||
{roleMappingMode === "fixedRoles" && (
|
||||
<div className="space-y-2 min-w-0 max-w-full">
|
||||
{restrictToOrgRoles ? (
|
||||
<RolesSelector
|
||||
selectedRoles={fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}))}
|
||||
mapRolesByName
|
||||
orgId={orgId as string}
|
||||
onSelectRoles={(nextTags) => {
|
||||
let names = [
|
||||
...new Set(nextTags.map((tag) => tag.text))
|
||||
];
|
||||
<RolesSelector
|
||||
selectedRoles={fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}))}
|
||||
mapRolesByName
|
||||
orgId={orgId as string}
|
||||
onSelectRoles={(nextTags) => {
|
||||
let names = [
|
||||
...new Set(nextTags.map((tag) => tag.text))
|
||||
];
|
||||
|
||||
if (!supportsMultipleRolesPerUser) {
|
||||
if (
|
||||
names.length === 0 &&
|
||||
fixedRoleNames.length > 0
|
||||
) {
|
||||
onFixedRoleNamesChange([
|
||||
fixedRoleNames[
|
||||
fixedRoleNames.length - 1
|
||||
]!
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (names.length > 1) {
|
||||
names = [names[names.length - 1]!];
|
||||
}
|
||||
if (!supportsMultipleRolesPerUser) {
|
||||
if (
|
||||
names.length === 0 &&
|
||||
fixedRoleNames.length > 0
|
||||
) {
|
||||
onFixedRoleNamesChange([
|
||||
fixedRoleNames[
|
||||
fixedRoleNames.length - 1
|
||||
]!
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
onFixedRoleNamesChange(names);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TagInput
|
||||
tags={fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}))}
|
||||
setTags={(nextTags) => {
|
||||
const prev = fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}));
|
||||
const next =
|
||||
typeof nextTags === "function"
|
||||
? nextTags(prev)
|
||||
: nextTags;
|
||||
|
||||
let names = [
|
||||
...new Set(next.map((tag) => tag.text))
|
||||
];
|
||||
|
||||
if (!supportsMultipleRolesPerUser) {
|
||||
if (
|
||||
names.length === 0 &&
|
||||
fixedRoleNames.length > 0
|
||||
) {
|
||||
onFixedRoleNamesChange([
|
||||
fixedRoleNames[
|
||||
fixedRoleNames.length - 1
|
||||
]!
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (names.length > 1) {
|
||||
names = [names[names.length - 1]!];
|
||||
}
|
||||
if (names.length > 1) {
|
||||
names = [names[names.length - 1]!];
|
||||
}
|
||||
}
|
||||
|
||||
onFixedRoleNamesChange(names);
|
||||
}}
|
||||
activeTagIndex={fixedRolesActiveTagIndex}
|
||||
setActiveTagIndex={setFixedRolesActiveTagIndex}
|
||||
placeholder={t(
|
||||
"roleMappingAssignRolesPlaceholderFreeform"
|
||||
)}
|
||||
enableAutocomplete={false}
|
||||
autocompleteOptions={roleOptions}
|
||||
restrictTagsToAutocompleteOptions={false}
|
||||
allowDuplicates={false}
|
||||
sortTags={true}
|
||||
size="sm"
|
||||
styleClasses={{
|
||||
inlineTagsContainer: "min-w-0 max-w-full"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onFixedRoleNamesChange(names);
|
||||
}}
|
||||
/>
|
||||
<FormDescription>
|
||||
{showFreeformRoleNamesHint
|
||||
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
||||
@@ -302,7 +242,6 @@ export default function RoleMappingConfigFields({
|
||||
showFreeformRoleNamesHint={
|
||||
showFreeformRoleNamesHint
|
||||
}
|
||||
orgId={orgId}
|
||||
supportsMultipleRolesPerUser={
|
||||
supportsMultipleRolesPerUser
|
||||
}
|
||||
@@ -379,8 +318,7 @@ function BuilderRuleRow({
|
||||
supportsMultipleRolesPerUser,
|
||||
showRemoveButton,
|
||||
onChange,
|
||||
onRemove,
|
||||
orgId
|
||||
onRemove
|
||||
}: {
|
||||
rule: MappingBuilderRule;
|
||||
roleOptions: Tag[];
|
||||
@@ -392,10 +330,10 @@ function BuilderRuleRow({
|
||||
showRemoveButton: boolean;
|
||||
onChange: (rule: MappingBuilderRule) => void;
|
||||
onRemove: () => void;
|
||||
orgId?: string;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||
const { orgId } = useParams();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -61,12 +61,19 @@ export function SettingsSectionBody({
|
||||
}
|
||||
|
||||
export function SettingsSectionFooter({
|
||||
children
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-2 mt-auto pt-6">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-2 mt-auto pt-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,11 +17,9 @@ import { useTranslations } from "next-intl";
|
||||
const TRIAL_DURATION_DAYS = 10;
|
||||
|
||||
export default function ShowTrialCard({
|
||||
isCollapsed,
|
||||
isOwner = false
|
||||
isCollapsed
|
||||
}: {
|
||||
isCollapsed?: boolean;
|
||||
isOwner?: boolean;
|
||||
}) {
|
||||
const context = useSubscriptionStatusContext();
|
||||
const params = useParams();
|
||||
@@ -34,55 +32,53 @@ export default function ShowTrialCard({
|
||||
|
||||
const now = Date.now();
|
||||
const remainingMs = trialExpiresAt - now;
|
||||
const remainingDays = Math.max(
|
||||
0,
|
||||
Math.ceil(remainingMs / (1000 * 60 * 60 * 24))
|
||||
);
|
||||
const remainingDays = Math.max(0, Math.ceil(remainingMs / (1000 * 60 * 60 * 24)));
|
||||
const totalMs = TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
|
||||
const progressPct = Math.min(
|
||||
100,
|
||||
Math.max(0, ((now - (trialExpiresAt - totalMs)) / totalMs) * 100)
|
||||
);
|
||||
const progressPct = Math.min(100, Math.max(0, ((now - (trialExpiresAt - totalMs)) / totalMs) * 100));
|
||||
// Inverted: full bar at start, drains to empty as trial ends
|
||||
const displayPct = 100 - progressPct;
|
||||
|
||||
const billingHref = orgId ? `/${orgId}/settings/billing` : "/";
|
||||
|
||||
if (isCollapsed) {
|
||||
const icon = (
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center justify-center rounded-md p-2 text-muted-foreground">
|
||||
<Link
|
||||
href={billingHref}
|
||||
className="flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 transition-colors"
|
||||
>
|
||||
<ClockIcon className="h-4 w-4 flex-none" />
|
||||
</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<p>
|
||||
{remainingDays === 0
|
||||
? t("trialExpired")
|
||||
: t("trialDaysLeftShort", {
|
||||
days: remainingDays
|
||||
})}
|
||||
: t("trialDaysLeftShort", { days: remainingDays })}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
if (isOwner) {
|
||||
return <Link href={billingHref}>{icon}</Link>;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
const cardContent = (
|
||||
<>
|
||||
return (
|
||||
<Link
|
||||
href={billingHref}
|
||||
className={cn(
|
||||
"group cursor-pointer block",
|
||||
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
||||
"transition duration-200 ease-in-out hover:bg-secondary/80 dark:hover:bg-secondary/60"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="flex-none size-4 text-muted-foreground" />
|
||||
<p className="font-medium flex-1 leading-tight">
|
||||
{remainingDays === 0 ? t("trialExpired") : t("trialActive")}
|
||||
{remainingDays === 0
|
||||
? t("trialExpired")
|
||||
: t("trialActive")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@@ -92,37 +88,11 @@ export default function ShowTrialCard({
|
||||
? t("trialHasEnded")
|
||||
: t("trialDaysRemaining", { count: remainingDays })}
|
||||
</small>
|
||||
{isOwner && (
|
||||
<div className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span>{t("trialGoToBilling")}</span>
|
||||
<ArrowRight className="flex-none size-3" />
|
||||
</div>
|
||||
)}
|
||||
<div className="inline-flex items-center gap-1 text-xs text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
<span>{t("trialGoToBilling")}</span>
|
||||
<ArrowRight className="flex-none size-3" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isOwner) {
|
||||
return (
|
||||
<Link
|
||||
href={billingHref}
|
||||
className={cn(
|
||||
"group cursor-pointer block",
|
||||
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm"
|
||||
)}
|
||||
>
|
||||
{cardContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm"
|
||||
)}
|
||||
>
|
||||
{cardContent}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,11 +25,15 @@ export function StrategySelect<TValue extends string>({
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols
|
||||
cols = 1
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<TValue | undefined>(defaultValue);
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<
|
||||
TValue | undefined
|
||||
>(defaultValue);
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const selected = isControlled ? (controlledValue ?? undefined) : uncontrolledSelected;
|
||||
const selected = isControlled
|
||||
? (controlledValue ?? undefined)
|
||||
: uncontrolledSelected;
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
@@ -39,7 +43,11 @@ export function StrategySelect<TValue extends string>({
|
||||
if (!isControlled) setUncontrolledSelected(typedValue);
|
||||
onChange?.(typedValue);
|
||||
}}
|
||||
className={`grid md:grid-cols-${cols ? cols : 1} gap-4`}
|
||||
style={{
|
||||
// @ts-expect-error
|
||||
"--cols": `repeat(${cols}, 1fr)`
|
||||
}}
|
||||
className="grid md:grid-cols-(--cols) gap-4"
|
||||
>
|
||||
{options.map((option: StrategyOption<TValue>) => (
|
||||
<label
|
||||
|
||||
@@ -53,12 +53,10 @@ export default function UptimeAlertSection({
|
||||
days = 90
|
||||
}: UptimeAlertSectionProps) {
|
||||
const t = useTranslations();
|
||||
const envContext = useEnvContext();
|
||||
const api = createApiClient(envContext);
|
||||
const api = createApiClient(useEnvContext());
|
||||
const queryClient = useQueryClient();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||
const { env } = envContext;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState(
|
||||
@@ -178,9 +176,7 @@ export default function UptimeAlertSection({
|
||||
{t("uptimeSectionDescription", { days })}
|
||||
</SettingsSectionDescription>
|
||||
</div>
|
||||
{!env.flags.disableEnterpriseFeatures
|
||||
? alertButton
|
||||
: null}
|
||||
{alertButton}
|
||||
</div>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
|
||||
@@ -23,6 +23,7 @@ export type MultiSelectTagsProps<T extends TagValue> = {
|
||||
onSearch: (query: string) => void;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
lockedIds?: Set<string>;
|
||||
};
|
||||
|
||||
export function MultiSelectContent<T extends TagValue>({
|
||||
@@ -32,7 +33,8 @@ export function MultiSelectContent<T extends TagValue>({
|
||||
value,
|
||||
options,
|
||||
onSearch,
|
||||
onChange
|
||||
onChange,
|
||||
lockedIds
|
||||
}: MultiSelectTagsProps<T>) {
|
||||
const t = useTranslations();
|
||||
const selectedValues = new Set(value.map((v) => v.id));
|
||||
@@ -48,33 +50,38 @@ export function MultiSelectContent<T extends TagValue>({
|
||||
{emptyPlaceholder ?? t("noResults")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
onSelect={() => {
|
||||
let newValues = [];
|
||||
if (selectedValues.has(option.id)) {
|
||||
newValues = value.filter(
|
||||
(v) => v.id !== option.id
|
||||
);
|
||||
} else {
|
||||
newValues = [...value, option];
|
||||
}
|
||||
onChange(newValues);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedValues.has(option.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{`${option.text}`}
|
||||
</CommandItem>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
const isLocked = lockedIds?.has(option.id);
|
||||
return (
|
||||
<CommandItem
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
disabled={isLocked}
|
||||
onSelect={() => {
|
||||
if (isLocked) return;
|
||||
let newValues = [];
|
||||
if (selectedValues.has(option.id)) {
|
||||
newValues = value.filter(
|
||||
(v) => v.id !== option.id
|
||||
);
|
||||
} else {
|
||||
newValues = [...value, option];
|
||||
}
|
||||
onChange(newValues);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedValues.has(option.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{`${option.text}`}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
||||
import { ChevronDownIcon, LockIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
type MultiSelectTagsProps,
|
||||
type TagValue,
|
||||
@@ -16,10 +16,12 @@ export interface MultiSelectInputProps<
|
||||
T extends TagValue
|
||||
> extends MultiSelectTagsProps<T> {
|
||||
buttonText?: string;
|
||||
lockedIds?: Set<string>;
|
||||
}
|
||||
|
||||
export function MultiSelectTagInput<T extends TagValue>({
|
||||
buttonText,
|
||||
lockedIds,
|
||||
...props
|
||||
}: MultiSelectInputProps<T>) {
|
||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
||||
@@ -52,46 +54,63 @@ export function MultiSelectTagInput<T extends TagValue>({
|
||||
"overflow-x-auto"
|
||||
)}
|
||||
>
|
||||
{props.value.map((option) => (
|
||||
<span
|
||||
key={option.id}
|
||||
className={cn(
|
||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
||||
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{option.text}
|
||||
<button
|
||||
className="p-0.5 flex-none cursor-pointer"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
let newValues = [];
|
||||
if (selectedValues.has(option.id)) {
|
||||
newValues = props.value.filter(
|
||||
(v) => v.id !== option.id
|
||||
);
|
||||
} else {
|
||||
newValues = [
|
||||
...props.value,
|
||||
option
|
||||
];
|
||||
}
|
||||
props.onChange(newValues);
|
||||
}}
|
||||
{props.value.map((option) => {
|
||||
const isLocked = lockedIds?.has(option.id);
|
||||
return (
|
||||
<span
|
||||
key={option.id}
|
||||
className={cn(
|
||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
||||
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5",
|
||||
isLocked && "opacity-60"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{option.text}
|
||||
{isLocked ? (
|
||||
<span className="p-0.5 flex-none">
|
||||
<LockIcon className="size-3" />
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className="p-0.5 flex-none cursor-pointer"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
let newValues = [];
|
||||
if (
|
||||
selectedValues.has(
|
||||
option.id
|
||||
)
|
||||
) {
|
||||
newValues =
|
||||
props.value.filter(
|
||||
(v) =>
|
||||
v.id !==
|
||||
option.id
|
||||
);
|
||||
} else {
|
||||
newValues = [
|
||||
...props.value,
|
||||
option
|
||||
];
|
||||
}
|
||||
props.onChange(newValues);
|
||||
}}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<span className="pl-1 font-normal">{buttonText}</span>
|
||||
</span>
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<MultiSelectContent {...props} />
|
||||
<MultiSelectContent {...props} lockedIds={lockedIds} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import z from "zod";
|
||||
|
||||
import { createPolicySchema, type PolicyFormValues } from ".";
|
||||
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot
|
||||
} from "@app/components/ui/input-otp";
|
||||
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { Binary, Bot, Key, Plus } from "lucide-react";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { type UseFormReturn, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
// ─── CreatePolicyAuthMethodsSectionForm ───────────────────────────────────────
|
||||
|
||||
const setPasswordSchema = z.object({
|
||||
password: z.string().min(4).max(100)
|
||||
});
|
||||
|
||||
const setPincodeSchema = z.object({
|
||||
pincode: z.string().length(6)
|
||||
});
|
||||
|
||||
const setHeaderAuthSchema = z.object({
|
||||
user: z.string().min(4).max(100),
|
||||
password: z.string().min(4).max(100),
|
||||
extendedCompatibility: z.boolean()
|
||||
});
|
||||
|
||||
export type CreatePolicyAuthMethodsSectionFormProps = {
|
||||
form: UseFormReturn<PolicyFormValues, any, any>;
|
||||
};
|
||||
|
||||
export function CreatePolicyAuthMethodsSectionForm({
|
||||
form: parentForm
|
||||
}: CreatePolicyAuthMethodsSectionFormProps) {
|
||||
const t = useTranslations();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false);
|
||||
const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false);
|
||||
const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
createPolicySchema.pick({
|
||||
password: true,
|
||||
pincode: true,
|
||||
headerAuth: true
|
||||
})
|
||||
),
|
||||
defaultValues: {
|
||||
password: null,
|
||||
pincode: null,
|
||||
headerAuth: null
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((values) => {
|
||||
parentForm.setValue("password", values.password as any);
|
||||
parentForm.setValue("pincode", values.pincode as any);
|
||||
parentForm.setValue("headerAuth", values.headerAuth as any);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, parentForm]);
|
||||
|
||||
const password = useWatch({
|
||||
control: form.control,
|
||||
name: "password"
|
||||
});
|
||||
const pincode = useWatch({
|
||||
control: form.control,
|
||||
name: "pincode"
|
||||
});
|
||||
const headerAuth = useWatch({
|
||||
control: form.control,
|
||||
name: "headerAuth"
|
||||
});
|
||||
|
||||
const passwordForm = useForm({
|
||||
resolver: zodResolver(setPasswordSchema),
|
||||
defaultValues: { password: "" }
|
||||
});
|
||||
|
||||
const pincodeForm = useForm({
|
||||
resolver: zodResolver(setPincodeSchema),
|
||||
defaultValues: { pincode: "" }
|
||||
});
|
||||
|
||||
const headerAuthForm = useForm({
|
||||
resolver: zodResolver(setHeaderAuthSchema),
|
||||
defaultValues: { user: "", password: "", extendedCompatibility: true }
|
||||
});
|
||||
|
||||
if (!isExpanded) {
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceAuthMethods")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicyAuthMethodsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyAuthMethodAdd")}
|
||||
</Button>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Password Credenza */}
|
||||
<Credenza
|
||||
open={isSetPasswordOpen}
|
||||
onOpenChange={(val) => {
|
||||
setIsSetPasswordOpen(val);
|
||||
if (!val) passwordForm.reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("resourcePasswordSetupTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("resourcePasswordSetupTitleDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...passwordForm}>
|
||||
<form
|
||||
onSubmit={passwordForm.handleSubmit((data) => {
|
||||
form.setValue("password", data);
|
||||
setIsSetPasswordOpen(false);
|
||||
passwordForm.reset();
|
||||
})}
|
||||
className="space-y-4"
|
||||
id="set-password-form"
|
||||
>
|
||||
<FormField
|
||||
control={passwordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("password")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button type="submit" form="set-password-form">
|
||||
{t("resourcePasswordSubmit")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
{/* Pincode Credenza */}
|
||||
<Credenza
|
||||
open={isSetPincodeOpen}
|
||||
onOpenChange={(val) => {
|
||||
setIsSetPincodeOpen(val);
|
||||
if (!val) pincodeForm.reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("resourcePincodeSetupTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("resourcePincodeSetupTitleDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...pincodeForm}>
|
||||
<form
|
||||
onSubmit={pincodeForm.handleSubmit((data) => {
|
||||
form.setValue("pincode", data);
|
||||
setIsSetPincodeOpen(false);
|
||||
pincodeForm.reset();
|
||||
})}
|
||||
className="space-y-4"
|
||||
id="set-pincode-form"
|
||||
>
|
||||
<FormField
|
||||
control={pincodeForm.control}
|
||||
name="pincode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("resourcePincode")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP
|
||||
autoComplete="false"
|
||||
maxLength={6}
|
||||
{...field}
|
||||
>
|
||||
<InputOTPGroup className="flex">
|
||||
<InputOTPSlot
|
||||
index={0}
|
||||
obscured
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={1}
|
||||
obscured
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={2}
|
||||
obscured
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={3}
|
||||
obscured
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={4}
|
||||
obscured
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={5}
|
||||
obscured
|
||||
/>
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button type="submit" form="set-pincode-form">
|
||||
{t("resourcePincodeSubmit")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
{/* Header Auth Credenza */}
|
||||
<Credenza
|
||||
open={isSetHeaderAuthOpen}
|
||||
onOpenChange={(val) => {
|
||||
setIsSetHeaderAuthOpen(val);
|
||||
if (!val) headerAuthForm.reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("resourceHeaderAuthSetupTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("resourceHeaderAuthSetupTitleDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...headerAuthForm}>
|
||||
<form
|
||||
onSubmit={headerAuthForm.handleSubmit(
|
||||
(data) => {
|
||||
form.setValue("headerAuth", data);
|
||||
setIsSetHeaderAuthOpen(false);
|
||||
headerAuthForm.reset();
|
||||
}
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="set-header-auth-form"
|
||||
>
|
||||
<FormField
|
||||
control={headerAuthForm.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("user")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={headerAuthForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("password")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={headerAuthForm.control}
|
||||
name="extendedCompatibility"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="header-auth-compatibility-toggle"
|
||||
label={t(
|
||||
"headerAuthCompatibility"
|
||||
)}
|
||||
info={t(
|
||||
"headerAuthCompatibilityInfo"
|
||||
)}
|
||||
checked={field.value}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button type="submit" form="set-header-auth-form">
|
||||
{t("resourceHeaderAuthSubmit")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceAuthMethods")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicyAuthMethodsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
{/* Password row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2 mb-4">
|
||||
<div
|
||||
className={cn("flex items-center text-sm space-x-2", password && "text-green-500")}
|
||||
>
|
||||
<Key size="14" />
|
||||
<span>
|
||||
{t("resourcePasswordProtection", {
|
||||
status: password
|
||||
? t("enabled")
|
||||
: t("disabled")
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
password
|
||||
? () => form.setValue("password", null)
|
||||
: () => setIsSetPasswordOpen(true)
|
||||
}
|
||||
>
|
||||
{password
|
||||
? t("passwordRemove")
|
||||
: t("passwordAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Pincode row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={cn("flex items-center space-x-2 text-sm", pincode && "text-green-500")}
|
||||
>
|
||||
<Binary size="14" />
|
||||
<span>
|
||||
{t("resourcePincodeProtection", {
|
||||
status: pincode
|
||||
? t("enabled")
|
||||
: t("disabled")
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
pincode
|
||||
? () => form.setValue("pincode", null)
|
||||
: () => setIsSetPincodeOpen(true)
|
||||
}
|
||||
>
|
||||
{pincode ? t("pincodeRemove") : t("pincodeAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Header auth row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={cn("flex items-center space-x-2 text-sm", headerAuth && "text-green-500")}
|
||||
>
|
||||
<Bot size="14" />
|
||||
<span>
|
||||
{headerAuth
|
||||
? t(
|
||||
"resourceHeaderAuthProtectionEnabled"
|
||||
)
|
||||
: t(
|
||||
"resourceHeaderAuthProtectionDisabled"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
headerAuth
|
||||
? () =>
|
||||
form.setValue("headerAuth", null)
|
||||
: () => setIsSetHeaderAuthOpen(true)
|
||||
}
|
||||
>
|
||||
{headerAuth
|
||||
? t("headerAuthRemove")
|
||||
: t("headerAuthAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
280
src/components/resource-policy/CreatePolicyForm.tsx
Normal file
280
src/components/resource-policy/CreatePolicyForm.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type PolicyFormValues, createPolicySchema } from ".";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { orgs, type ResourcePolicy } from "@server/db";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useMemo, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CreatePolicyUsersRolesSectionForm } from "./CreatePolicyUserRolesSectionForm";
|
||||
import { CreatePolicyAuthMethodsSectionForm } from "./CreatePolicyAuthMethodsSectionForm";
|
||||
import { CreatePolicyOtpEmailSectionForm } from "./CreatePolicyOtpEmailSectionForm";
|
||||
import { CreatePolicyRulesSectionForm } from "./CreatePolicyRulesSectionForm";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
// ─── CreatePolicyForm ─────────────────────────────────────────────────────────
|
||||
|
||||
export type CreatePolicyFormProps = {};
|
||||
|
||||
export function CreatePolicyForm({}: CreatePolicyFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isMaxmindAvailable = !!(
|
||||
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0
|
||||
);
|
||||
const isMaxmindAsnAvailable = !!(
|
||||
env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0
|
||||
);
|
||||
|
||||
const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery(
|
||||
orgQueries.roles({ orgId: org.org.orgId })
|
||||
);
|
||||
const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery(
|
||||
orgQueries.users({ orgId: org.org.orgId })
|
||||
);
|
||||
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
|
||||
orgQueries.identityProviders({
|
||||
orgId: org.org.orgId,
|
||||
useOrgOnlyIdp: env.app.identityProviderMode === "org"
|
||||
})
|
||||
);
|
||||
|
||||
const form = useForm<PolicyFormValues>({
|
||||
resolver: zodResolver(createPolicySchema) as any,
|
||||
defaultValues: {
|
||||
name: "",
|
||||
sso: true,
|
||||
skipToIdpId: null,
|
||||
emailWhitelistEnabled: false,
|
||||
roles: [],
|
||||
users: [],
|
||||
emails: [],
|
||||
applyRules: false,
|
||||
rules: [],
|
||||
password: null,
|
||||
headerAuth: null,
|
||||
pincode: null
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
const payload = form.getValues();
|
||||
|
||||
try {
|
||||
const res = await api
|
||||
.post<AxiosResponse<ResourcePolicy>>(
|
||||
`/org/${org.org.orgId}/resource-policy/`,
|
||||
{
|
||||
name: payload.name,
|
||||
// access control
|
||||
sso: payload.sso,
|
||||
roleIds: payload.roles.map((r) => r.id),
|
||||
userIds: payload.users.map((u) => u.id),
|
||||
skipToIdpId: payload.skipToIdpId,
|
||||
// auth methods
|
||||
password: payload.password?.password,
|
||||
pincode: payload.pincode?.pincode,
|
||||
headerAuth: payload.headerAuth,
|
||||
// email OTP
|
||||
emailWhitelistEnabled: payload.emailWhitelistEnabled,
|
||||
emails: payload.emails.map((email) => email.text),
|
||||
// rules
|
||||
applyRules: payload.applyRules,
|
||||
rules: payload.rules
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("policyErrorCreate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("policyErrorCreateDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 201) {
|
||||
const niceId = res.data.data.niceId;
|
||||
router.push(
|
||||
`/${org.org.orgId}/settings/policies/resource/${niceId}`
|
||||
);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("policyCreatedSuccess")
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("policyErrorCreate"),
|
||||
description: t("policyErrorCreateMessageDescription")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allRoles = useMemo(
|
||||
() =>
|
||||
orgRoles
|
||||
.map((role) => ({
|
||||
id: role.roleId.toString(),
|
||||
text: role.name
|
||||
}))
|
||||
.filter((role) => role.text !== "Admin"),
|
||||
[orgRoles]
|
||||
);
|
||||
|
||||
const allUsers = useMemo(
|
||||
() =>
|
||||
orgUsers.map((user) => ({
|
||||
id: user.id.toString(),
|
||||
text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
||||
})),
|
||||
[orgUsers]
|
||||
);
|
||||
|
||||
const allIdps = useMemo(() => {
|
||||
if (build === "saas") {
|
||||
if (isPaidUser(tierMatrix.orgOidc)) {
|
||||
return orgIdps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
return orgIdps.map((idp) => ({ id: idp.idpId, text: idp.name }));
|
||||
}
|
||||
return [];
|
||||
}, [orgIdps, isPaidUser]);
|
||||
|
||||
if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
||||
const isDisabled = !isPaidUser(policyTiers);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<Form {...form}>
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? "pointer-events-none opacity-50"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<SettingsContainer>
|
||||
{/* Name */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourcePolicyName")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicyNameDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"resourcePolicyNamePlaceholder"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<CreatePolicyUsersRolesSectionForm
|
||||
form={form}
|
||||
allRoles={allRoles}
|
||||
allUsers={allUsers}
|
||||
allIdps={allIdps}
|
||||
/>
|
||||
<CreatePolicyAuthMethodsSectionForm form={form} />
|
||||
<CreatePolicyOtpEmailSectionForm
|
||||
form={form}
|
||||
emailEnabled={env.email.emailEnabled}
|
||||
/>
|
||||
<CreatePolicyRulesSectionForm
|
||||
form={form}
|
||||
isMaxmindAvailable={isMaxmindAvailable}
|
||||
isMaxmindAsnAvailable={isMaxmindAsnAvailable}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</div>
|
||||
|
||||
<div className="flex py-6 justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => startTransition(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
>
|
||||
{t("resourcePoliciesCreate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user