mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 17:29:54 +00:00
Compare commits
101 Commits
resource-p
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10f95896aa | ||
|
|
5b8994d143 | ||
|
|
c46ef2fe9c | ||
|
|
4cd025dd91 | ||
|
|
ce04ea9720 | ||
|
|
a3ce382725 | ||
|
|
4eb49e3e60 | ||
|
|
2a9481023a | ||
|
|
8ed01372b8 | ||
|
|
6a7d4fd385 | ||
|
|
7bc08c0425 | ||
|
|
36a47c4cfb | ||
|
|
7dce4500ec | ||
|
|
72e48a56df | ||
|
|
293d9865b4 | ||
|
|
45a2a07747 | ||
|
|
181bcffe7d | ||
|
|
ed35d25598 | ||
|
|
05e738e0f4 | ||
|
|
c95e66d531 | ||
|
|
cc2a416a92 | ||
|
|
70bb42f1fc | ||
|
|
10d2bc1e9e | ||
|
|
385f57ec93 | ||
|
|
9c8ffdb661 | ||
|
|
5a5feccc76 | ||
|
|
36e7054386 | ||
|
|
19de12b12e | ||
|
|
d96e930679 | ||
|
|
5e51b8ad74 | ||
|
|
885b9e638d | ||
|
|
56ef3a934a | ||
|
|
98bc199c8e | ||
|
|
0444d3490b | ||
|
|
54820d1db0 | ||
|
|
961cbfcacc | ||
|
|
a784cd307e | ||
|
|
b46c948522 | ||
|
|
7eab2cc0bb | ||
|
|
5ff2569ece | ||
|
|
c59505be8d | ||
|
|
2b0e6649fa | ||
|
|
428e9b546e | ||
|
|
5089660381 | ||
|
|
998364b09d | ||
|
|
ac0d88d9b7 | ||
|
|
401f04b53e | ||
|
|
b046ab7513 | ||
|
|
65ee9b9544 | ||
|
|
49c7319342 | ||
|
|
ce7df5ddaa | ||
|
|
af1739fbcb | ||
|
|
f01c9ee41c | ||
|
|
19f8956218 | ||
|
|
a8c50b8618 | ||
|
|
e86a381ed5 | ||
|
|
dd18375f23 | ||
|
|
46b72b9e8c | ||
|
|
7bb2a5a0a5 | ||
|
|
4b777b1488 | ||
|
|
428f91b5fa | ||
|
|
caaae77f74 | ||
|
|
4df27b316c | ||
|
|
8f52a48937 | ||
|
|
a53da85fb4 | ||
|
|
08a5785cc5 | ||
|
|
ff928b846d | ||
|
|
47b3d26d0e | ||
|
|
6270dce86a | ||
|
|
864d1d5cc4 | ||
|
|
b63eda64f4 | ||
|
|
b8e942478d | ||
|
|
6d9bfbf08f | ||
|
|
35ce947e19 | ||
|
|
b17ba96235 | ||
|
|
f1bdb25497 | ||
|
|
e11527b430 | ||
|
|
31d3b314e9 | ||
|
|
3bce57c65c | ||
|
|
d649a83535 | ||
|
|
3c6b1781bc | ||
|
|
7dd50f65fc | ||
|
|
342b4aeddf | ||
|
|
65908fa00f | ||
|
|
223e0d0706 | ||
|
|
5426031cd4 | ||
|
|
adf4a1ffda | ||
|
|
780feba19c | ||
|
|
3ac315b52e | ||
|
|
1b183d32c0 | ||
|
|
0c643e91a6 | ||
|
|
fab53ba26a | ||
|
|
62e19a2f4e | ||
|
|
7d67fb9984 | ||
|
|
432dc81875 | ||
|
|
9b71c426c7 | ||
|
|
87e6c7ba36 | ||
|
|
9410a18404 | ||
|
|
23f4302186 | ||
|
|
fb4bda077b | ||
|
|
cf596d980f |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,9 +17,9 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.db
|
*.db
|
||||||
*.sqlite*
|
*.sqlite
|
||||||
!Dockerfile.sqlite
|
!Dockerfile.sqlite
|
||||||
*.sqlite3*
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
.machinelogs*.json
|
.machinelogs*.json
|
||||||
*-audit.json
|
*-audit.json
|
||||||
@@ -54,4 +54,3 @@ hydrateSaas.ts
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
drizzle.config.ts
|
drizzle.config.ts
|
||||||
server/setup/migrations.ts
|
server/setup/migrations.ts
|
||||||
solo.yml
|
|
||||||
28
cli/commands/clearCertificates.ts
Normal file
28
cli/commands/clearCertificates.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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,6 +9,7 @@ import { rotateServerSecret } from "./commands/rotateServerSecret";
|
|||||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||||
import { deleteClient } from "./commands/deleteClient";
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
|
import { clearCertificates } from "./commands/clearCertificates";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -19,5 +20,6 @@ yargs(hideBin(process.argv))
|
|||||||
.command(clearLicenseKeys)
|
.command(clearLicenseKeys)
|
||||||
.command(deleteClient)
|
.command(deleteClient)
|
||||||
.command(generateOrgCaKeys)
|
.command(generateOrgCaKeys)
|
||||||
|
.command(clearCertificates)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Няма валидни методи за удостоверение",
|
"noMoreAuthMethods": "Няма валидни методи за удостоверение",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Причина",
|
"reason": "Причина",
|
||||||
"requestLogs": "Заявка за логове",
|
"requestLogs": "Логове за HTTP заявки",
|
||||||
"requestAnalytics": "Анализи На Заявки",
|
"requestAnalytics": "Анализи На Заявки",
|
||||||
"host": "Хост",
|
"host": "Хост",
|
||||||
"location": "Местоположение",
|
"location": "Местоположение",
|
||||||
"actionLogs": "Дневници на действията",
|
"actionLogs": "Дневници на действията",
|
||||||
"sidebarLogsRequest": "Заявка за логове",
|
"sidebarLogsRequest": "Логове за HTTP заявки",
|
||||||
"sidebarLogsAccess": "Достъп до логове",
|
"sidebarLogsAccess": "Достъп до логове",
|
||||||
"sidebarLogsAction": "Дневници на действията",
|
"sidebarLogsAction": "Дневници на действията",
|
||||||
"logRetention": "Задържане на логове",
|
"logRetention": "Задържане на логове",
|
||||||
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
||||||
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
||||||
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
||||||
"logRetentionRequestLabel": "Задържане на логове на заявки",
|
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
|
||||||
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
||||||
"logRetentionAccessLabel": "Задържане на логове за достъп",
|
"logRetentionAccessLabel": "Задържане на логове за достъп",
|
||||||
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
|
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
||||||
"httpDestConnectionLogsTitle": "Логове на връзката",
|
"httpDestConnectionLogsTitle": "Логове на връзката",
|
||||||
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
||||||
"httpDestRequestLogsTitle": "Заявки за логове",
|
"httpDestRequestLogsTitle": "Логове за HTTP заявки",
|
||||||
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
||||||
"httpDestSaveChanges": "Запази промените",
|
"httpDestSaveChanges": "Запази промените",
|
||||||
"httpDestCreateDestination": "Създаване на дестинация",
|
"httpDestCreateDestination": "Създаване на дестинация",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
|
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
|
||||||
"domainPickerWildcardCertWarningLink": "Научете повече",
|
"domainPickerWildcardCertWarningLink": "Научете повече",
|
||||||
"health": "Здраве",
|
"health": "Здраве",
|
||||||
"domainPendingErrorTitle": "Проблем при проверка"
|
"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": "Следващ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP adresa",
|
"ip": "IP adresa",
|
||||||
"reason": "Důvod",
|
"reason": "Důvod",
|
||||||
"requestLogs": "Záznamy požadavků",
|
"requestLogs": "Záznamy HTTP požadavků",
|
||||||
"requestAnalytics": "Vyžádat analýzu",
|
"requestAnalytics": "Vyžádat analýzu",
|
||||||
"host": "Hostitel",
|
"host": "Hostitel",
|
||||||
"location": "Poloha",
|
"location": "Poloha",
|
||||||
"actionLogs": "Záznamy akcí",
|
"actionLogs": "Záznamy akcí",
|
||||||
"sidebarLogsRequest": "Záznamy požadavků",
|
"sidebarLogsRequest": "Záznamy HTTP požadavků",
|
||||||
"sidebarLogsAccess": "Protokoly přístupu",
|
"sidebarLogsAccess": "Protokoly přístupu",
|
||||||
"sidebarLogsAction": "Záznamy akcí",
|
"sidebarLogsAction": "Záznamy akcí",
|
||||||
"logRetention": "Zaznamenávání záznamu",
|
"logRetention": "Zaznamenávání záznamu",
|
||||||
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
"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",
|
"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",
|
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
||||||
"logRetentionRequestLabel": "Zachování logu žádosti",
|
"logRetentionRequestLabel": "Zachování logu HTTP požadavků",
|
||||||
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
||||||
"logRetentionAccessLabel": "Zachování záznamu přístupu",
|
"logRetentionAccessLabel": "Zachování záznamu přístupu",
|
||||||
"logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy",
|
"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.",
|
"httpDestActionLogsDescription": "Správní opatření prováděná uživateli v rámci organizace.",
|
||||||
"httpDestConnectionLogsTitle": "Protokoly připojení",
|
"httpDestConnectionLogsTitle": "Protokoly připojení",
|
||||||
"httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.",
|
"httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.",
|
||||||
"httpDestRequestLogsTitle": "Záznamy požadavků",
|
"httpDestRequestLogsTitle": "Záznamy HTTP požadavků",
|
||||||
"httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.",
|
"httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.",
|
||||||
"httpDestSaveChanges": "Uložit změny",
|
"httpDestSaveChanges": "Uložit změny",
|
||||||
"httpDestCreateDestination": "Vytvořit cíl",
|
"httpDestCreateDestination": "Vytvořit cíl",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
|
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
|
||||||
"domainPickerWildcardCertWarningLink": "Zjistit více",
|
"domainPickerWildcardCertWarningLink": "Zjistit více",
|
||||||
"health": "Zdraví",
|
"health": "Zdraví",
|
||||||
"domainPendingErrorTitle": "Problém s ověřením"
|
"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í"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Keine gültige Authentifizierungsmethode verfügbar",
|
"noMoreAuthMethods": "Keine gültige Authentifizierungsmethode verfügbar",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Grund",
|
"reason": "Grund",
|
||||||
"requestLogs": "Logs anfordern",
|
"requestLogs": "HTTP Anforderungsprotokolle",
|
||||||
"requestAnalytics": "Anfrage-Analyse anzeigen",
|
"requestAnalytics": "Anfrage-Analyse anzeigen",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Standort",
|
"location": "Standort",
|
||||||
"actionLogs": "Aktionsprotokolle",
|
"actionLogs": "Aktionsprotokolle",
|
||||||
"sidebarLogsRequest": "Logs anfordern",
|
"sidebarLogsRequest": "HTTP Anforderungsprotokolle",
|
||||||
"sidebarLogsAccess": "Zugriffsprotokolle",
|
"sidebarLogsAccess": "Zugriffsprotokolle",
|
||||||
"sidebarLogsAction": "Aktionsprotokolle",
|
"sidebarLogsAction": "Aktionsprotokolle",
|
||||||
"logRetention": "Log-Speicherung",
|
"logRetention": "Log-Speicherung",
|
||||||
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
"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",
|
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
||||||
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
||||||
"logRetentionRequestLabel": "Log-Speicherung anfordern",
|
"logRetentionRequestLabel": "HTTP Anforderungsprotokoll Aufbewahrung",
|
||||||
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
||||||
"logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung",
|
"logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung",
|
||||||
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
||||||
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
||||||
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
||||||
"httpDestRequestLogsTitle": "Logs anfordern",
|
"httpDestRequestLogsTitle": "HTTP Anforderungsprotokolle",
|
||||||
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
||||||
"httpDestSaveChanges": "Änderungen speichern",
|
"httpDestSaveChanges": "Änderungen speichern",
|
||||||
"httpDestCreateDestination": "Ziel erstellen",
|
"httpDestCreateDestination": "Ziel erstellen",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
|
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
|
||||||
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
|
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
|
||||||
"health": "Gesundheit",
|
"health": "Gesundheit",
|
||||||
"domainPendingErrorTitle": "Verifizierungsproblem"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,33 +204,11 @@
|
|||||||
"resourcesSearch": "Search resources...",
|
"resourcesSearch": "Search resources...",
|
||||||
"resourceAdd": "Add Resource",
|
"resourceAdd": "Add Resource",
|
||||||
"resourceErrorDelte": "Error deleting 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",
|
"authentication": "Authentication",
|
||||||
"protected": "Protected",
|
"protected": "Protected",
|
||||||
"notProtected": "Not Protected",
|
"notProtected": "Not Protected",
|
||||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
"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?",
|
"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",
|
"resourceHTTP": "HTTPS Resource",
|
||||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
@@ -271,8 +249,6 @@
|
|||||||
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
||||||
"resourceBack": "Back to Resources",
|
"resourceBack": "Back to Resources",
|
||||||
"resourceGoTo": "Go to Resource",
|
"resourceGoTo": "Go to Resource",
|
||||||
"resourcePolicyDelete": "Delete Resource Policy",
|
|
||||||
"resourcePolicyDeleteConfirm": "Confirm Delete Resource Policy",
|
|
||||||
"resourceDelete": "Delete Resource",
|
"resourceDelete": "Delete Resource",
|
||||||
"resourceDeleteConfirm": "Confirm Delete Resource",
|
"resourceDeleteConfirm": "Confirm Delete Resource",
|
||||||
"visibility": "Visibility",
|
"visibility": "Visibility",
|
||||||
@@ -285,8 +261,6 @@
|
|||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on the resource",
|
"resourceSettingDescription": "Configure the settings on the resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
"resourcePolicySettingDescription": "Configure the settings on the resource policy",
|
|
||||||
"resourcePolicySetting": "{policyName} Settings",
|
|
||||||
"alwaysAllow": "Bypass Auth",
|
"alwaysAllow": "Bypass Auth",
|
||||||
"alwaysDeny": "Block Access",
|
"alwaysDeny": "Block Access",
|
||||||
"passToAuth": "Pass to Auth",
|
"passToAuth": "Pass to Auth",
|
||||||
@@ -757,16 +731,6 @@
|
|||||||
"rulesNoOne": "No rules. Add a rule using the form.",
|
"rulesNoOne": "No rules. Add a rule using the form.",
|
||||||
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
||||||
"rulesSubmit": "Save Rules",
|
"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",
|
"resourceErrorCreate": "Error creating resource",
|
||||||
"resourceErrorCreateDescription": "An error occurred when creating the resource",
|
"resourceErrorCreateDescription": "An error occurred when creating the resource",
|
||||||
"resourceErrorCreateMessage": "Error creating resource:",
|
"resourceErrorCreateMessage": "Error creating resource:",
|
||||||
@@ -830,16 +794,6 @@
|
|||||||
"pincodeAdd": "Add PIN Code",
|
"pincodeAdd": "Add PIN Code",
|
||||||
"pincodeRemove": "Remove PIN Code",
|
"pincodeRemove": "Remove PIN Code",
|
||||||
"resourceAuthMethods": "Authentication Methods",
|
"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",
|
"resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods",
|
||||||
"resourceAuthSettingsSave": "Saved successfully",
|
"resourceAuthSettingsSave": "Saved successfully",
|
||||||
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
|
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
|
||||||
@@ -875,12 +829,6 @@
|
|||||||
"resourcePincodeSetupTitle": "Set Pincode",
|
"resourcePincodeSetupTitle": "Set Pincode",
|
||||||
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
||||||
"resourceRoleDescription": "Admins can always access 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",
|
"resourceUsersRoles": "Access Controls",
|
||||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||||
"resourceUsersRolesSubmit": "Save Access Controls",
|
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||||
@@ -1410,8 +1358,6 @@
|
|||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
"sidebarProxyResources": "Public",
|
"sidebarProxyResources": "Public",
|
||||||
"sidebarClientResources": "Private",
|
"sidebarClientResources": "Private",
|
||||||
"sidebarPolicies": "Policies",
|
|
||||||
"sidebarResourcePolicies": "Resources",
|
|
||||||
"sidebarAccessControl": "Access Control",
|
"sidebarAccessControl": "Access Control",
|
||||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||||
"sidebarTeam": "Team",
|
"sidebarTeam": "Team",
|
||||||
@@ -2714,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"requestLogs": "HTTPS Request Logs",
|
"requestLogs": "HTTP Request Logs",
|
||||||
"requestAnalytics": "Request Analytics",
|
"requestAnalytics": "Request Analytics",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"actionLogs": "Admin Action Logs",
|
"actionLogs": "Admin Action Logs",
|
||||||
"sidebarLogsRequest": "HTTPS Request Logs",
|
"sidebarLogsRequest": "HTTP Request Logs",
|
||||||
"sidebarLogsAccess": "Authentication Logs",
|
"sidebarLogsAccess": "Authentication Logs",
|
||||||
"sidebarLogsAction": "Admin Action Logs",
|
"sidebarLogsAction": "Admin Action Logs",
|
||||||
"logRetention": "Log Retention",
|
"logRetention": "Log Retention",
|
||||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||||
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
||||||
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||||
"logRetentionRequestLabel": "HTTPS Request Log Retention",
|
"logRetentionRequestLabel": "HTTP Request Log Retention",
|
||||||
"logRetentionRequestDescription": "How long to retain request logs",
|
"logRetentionRequestDescription": "How long to retain request logs",
|
||||||
"logRetentionAccessLabel": "Authentication Log Retention",
|
"logRetentionAccessLabel": "Authentication Log Retention",
|
||||||
"logRetentionAccessDescription": "How long to retain access logs",
|
"logRetentionAccessDescription": "How long to retain access logs",
|
||||||
@@ -3188,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||||
"httpDestConnectionLogsTitle": "Network Logs",
|
"httpDestConnectionLogsTitle": "Network Logs",
|
||||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||||
"httpDestRequestLogsTitle": "HTTPS Request Logs",
|
"httpDestRequestLogsTitle": "HTTP Request Logs",
|
||||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||||
"httpDestSaveChanges": "Save Changes",
|
"httpDestSaveChanges": "Save Changes",
|
||||||
"httpDestCreateDestination": "Create Destination",
|
"httpDestCreateDestination": "Create Destination",
|
||||||
@@ -3262,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
|
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
|
||||||
"domainPickerWildcardCertWarningLink": "Learn more",
|
"domainPickerWildcardCertWarningLink": "Learn more",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"domainPendingErrorTitle": "Verification Issue"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Razón",
|
"reason": "Razón",
|
||||||
"requestLogs": "Registros de Solicitud",
|
"requestLogs": "Registros de Solicitud HTTP",
|
||||||
"requestAnalytics": "Analítica de Solicitud",
|
"requestAnalytics": "Analítica de Solicitud",
|
||||||
"host": "Anfitrión",
|
"host": "Anfitrión",
|
||||||
"location": "Ubicación",
|
"location": "Ubicación",
|
||||||
"actionLogs": "Registros de acción",
|
"actionLogs": "Registros de acción",
|
||||||
"sidebarLogsRequest": "Registros de Solicitud",
|
"sidebarLogsRequest": "Registros de Solicitud HTTP",
|
||||||
"sidebarLogsAccess": "Registros de acceso",
|
"sidebarLogsAccess": "Registros de acceso",
|
||||||
"sidebarLogsAction": "Registros de acción",
|
"sidebarLogsAction": "Registros de acción",
|
||||||
"logRetention": "Retención de Log",
|
"logRetention": "Retención de Log",
|
||||||
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
"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",
|
"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",
|
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
||||||
"logRetentionRequestLabel": "Retención de Registro de Solicitud",
|
"logRetentionRequestLabel": "Retención de Registro de Solicitud HTTP",
|
||||||
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
||||||
"logRetentionAccessLabel": "Retención de Log de Acceso",
|
"logRetentionAccessLabel": "Retención de Log de Acceso",
|
||||||
"logRetentionAccessDescription": "Cuánto tiempo retener los registros 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.",
|
"httpDestActionLogsDescription": "Acciones administrativas realizadas por los usuarios dentro de la organización.",
|
||||||
"httpDestConnectionLogsTitle": "Registros de conexión",
|
"httpDestConnectionLogsTitle": "Registros de conexión",
|
||||||
"httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.",
|
"httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.",
|
||||||
"httpDestRequestLogsTitle": "Registros de Solicitud",
|
"httpDestRequestLogsTitle": "Registros de Solicitud HTTP",
|
||||||
"httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.",
|
"httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.",
|
||||||
"httpDestSaveChanges": "Guardar Cambios",
|
"httpDestSaveChanges": "Guardar Cambios",
|
||||||
"httpDestCreateDestination": "Crear destino",
|
"httpDestCreateDestination": "Crear destino",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
|
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Más información",
|
"domainPickerWildcardCertWarningLink": "Más información",
|
||||||
"health": "Salud",
|
"health": "Salud",
|
||||||
"domainPendingErrorTitle": "Problema de verificación"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Raison",
|
"reason": "Raison",
|
||||||
"requestLogs": "Journal des requêtes",
|
"requestLogs": "Journal des Requêtes HTTP",
|
||||||
"requestAnalytics": "Demander des analyses",
|
"requestAnalytics": "Demander des analyses",
|
||||||
"host": "Hôte",
|
"host": "Hôte",
|
||||||
"location": "Localisation",
|
"location": "Localisation",
|
||||||
"actionLogs": "Journaux des actions",
|
"actionLogs": "Journaux des actions",
|
||||||
"sidebarLogsRequest": "Journal des requêtes",
|
"sidebarLogsRequest": "Journal des Requêtes HTTP",
|
||||||
"sidebarLogsAccess": "Journaux d'accès",
|
"sidebarLogsAccess": "Journaux d'accès",
|
||||||
"sidebarLogsAction": "Journaux des actions",
|
"sidebarLogsAction": "Journaux des actions",
|
||||||
"logRetention": "Journaliser la rétention",
|
"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",
|
"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",
|
"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",
|
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
||||||
"logRetentionRequestLabel": "Demander la rétention des journaux",
|
"logRetentionRequestLabel": "Rétention des Journaux de Requêtes HTTP",
|
||||||
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
||||||
"logRetentionAccessLabel": "Rétention du journal d'accès",
|
"logRetentionAccessLabel": "Rétention du journal d'accès",
|
||||||
"logRetentionAccessDescription": "Durée de conservation des journaux 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.",
|
"httpDestActionLogsDescription": "Actions administratives effectuées par les utilisateurs au sein de l'organisation.",
|
||||||
"httpDestConnectionLogsTitle": "Journaux de connexion",
|
"httpDestConnectionLogsTitle": "Journaux de connexion",
|
||||||
"httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.",
|
"httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.",
|
||||||
"httpDestRequestLogsTitle": "Journal des requêtes",
|
"httpDestRequestLogsTitle": "Journal des Requêtes HTTP",
|
||||||
"httpDestRequestLogsDescription": "Journaux des requêtes HTTP pour les ressources proxiées, y compris la méthode, le chemin et le code de réponse.",
|
"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",
|
"httpDestSaveChanges": "Enregistrer les modifications",
|
||||||
"httpDestCreateDestination": "Créer une destination",
|
"httpDestCreateDestination": "Créer une destination",
|
||||||
@@ -3209,5 +3209,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
|
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
|
||||||
"domainPickerWildcardCertWarningLink": "En savoir plus",
|
"domainPickerWildcardCertWarningLink": "En savoir plus",
|
||||||
"health": "Santé",
|
"health": "Santé",
|
||||||
"domainPendingErrorTitle": "Problème de vérification"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Motivo",
|
"reason": "Motivo",
|
||||||
"requestLogs": "Log Richiesta",
|
"requestLogs": "Log Richieste HTTP",
|
||||||
"requestAnalytics": "Richiedi Analisi",
|
"requestAnalytics": "Richiedi Analisi",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Posizione",
|
"location": "Posizione",
|
||||||
"actionLogs": "Log Azioni",
|
"actionLogs": "Log Azioni",
|
||||||
"sidebarLogsRequest": "Log Richiesta",
|
"sidebarLogsRequest": "Log Richieste HTTP",
|
||||||
"sidebarLogsAccess": "Log Accesso",
|
"sidebarLogsAccess": "Log Accesso",
|
||||||
"sidebarLogsAction": "Log Azioni",
|
"sidebarLogsAction": "Log Azioni",
|
||||||
"logRetention": "Ritenzione Registro",
|
"logRetention": "Ritenzione Registro",
|
||||||
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
"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",
|
"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",
|
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
||||||
"logRetentionRequestLabel": "Richiedi Ritenzione Log",
|
"logRetentionRequestLabel": "Conservazione Log Richieste HTTP",
|
||||||
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
||||||
"logRetentionAccessLabel": "Ritenzione Registro Accesso",
|
"logRetentionAccessLabel": "Ritenzione Registro Accesso",
|
||||||
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
|
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.",
|
"httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.",
|
||||||
"httpDestConnectionLogsTitle": "Log Di Connessione",
|
"httpDestConnectionLogsTitle": "Log Di Connessione",
|
||||||
"httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.",
|
"httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.",
|
||||||
"httpDestRequestLogsTitle": "Log Richiesta",
|
"httpDestRequestLogsTitle": "Log Richieste HTTP",
|
||||||
"httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.",
|
"httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.",
|
||||||
"httpDestSaveChanges": "Salva Modifiche",
|
"httpDestSaveChanges": "Salva Modifiche",
|
||||||
"httpDestCreateDestination": "Crea Destinazione",
|
"httpDestCreateDestination": "Crea Destinazione",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
|
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Scopri di più",
|
"domainPickerWildcardCertWarningLink": "Scopri di più",
|
||||||
"health": "Salute",
|
"health": "Salute",
|
||||||
"domainPendingErrorTitle": "Problema di Verifica"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "유효한 인증 없음",
|
"noMoreAuthMethods": "유효한 인증 없음",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "이유",
|
"reason": "이유",
|
||||||
"requestLogs": "요청 로그",
|
"requestLogs": "HTTP 요청 로그",
|
||||||
"requestAnalytics": "요청 분석",
|
"requestAnalytics": "요청 분석",
|
||||||
"host": "호스트",
|
"host": "호스트",
|
||||||
"location": "위치",
|
"location": "위치",
|
||||||
"actionLogs": "작업 로그",
|
"actionLogs": "작업 로그",
|
||||||
"sidebarLogsRequest": "요청 로그",
|
"sidebarLogsRequest": "HTTP 요청 로그",
|
||||||
"sidebarLogsAccess": "접근 로그",
|
"sidebarLogsAccess": "접근 로그",
|
||||||
"sidebarLogsAction": "작업 로그",
|
"sidebarLogsAction": "작업 로그",
|
||||||
"logRetention": "로그 보관",
|
"logRetention": "로그 보관",
|
||||||
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
||||||
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
||||||
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
||||||
"logRetentionRequestLabel": "요청 로그 보관",
|
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
|
||||||
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
||||||
"logRetentionAccessLabel": "접근 로그 보관",
|
"logRetentionAccessLabel": "접근 로그 보관",
|
||||||
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
||||||
"httpDestConnectionLogsTitle": "연결 로그",
|
"httpDestConnectionLogsTitle": "연결 로그",
|
||||||
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
||||||
"httpDestRequestLogsTitle": "요청 로그",
|
"httpDestRequestLogsTitle": "HTTP 요청 로그",
|
||||||
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
||||||
"httpDestSaveChanges": "변경 사항 저장",
|
"httpDestSaveChanges": "변경 사항 저장",
|
||||||
"httpDestCreateDestination": "대상지 생성",
|
"httpDestCreateDestination": "대상지 생성",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
|
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
|
||||||
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
|
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
|
||||||
"health": "건강",
|
"health": "건강",
|
||||||
"domainPendingErrorTitle": "확인 문제"
|
"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": "다음"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Grunn",
|
"reason": "Grunn",
|
||||||
"requestLogs": "Forespørselslogger (Automatic Translation)",
|
"requestLogs": "HTTP-forespørselslogger",
|
||||||
"requestAnalytics": "Be om analyser",
|
"requestAnalytics": "Be om analyser",
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
"location": "Sted",
|
"location": "Sted",
|
||||||
"actionLogs": "Handlingslogger",
|
"actionLogs": "Handlingslogger",
|
||||||
"sidebarLogsRequest": "Forespørselslogger (Automatic Translation)",
|
"sidebarLogsRequest": "HTTP-forespørselslogger",
|
||||||
"sidebarLogsAccess": "Tilgangslogger (Automatic Translation)",
|
"sidebarLogsAccess": "Tilgangslogger (Automatic Translation)",
|
||||||
"sidebarLogsAction": "Handlingslogger",
|
"sidebarLogsAction": "Handlingslogger",
|
||||||
"logRetention": "Logg tilbaketrekning",
|
"logRetention": "Logg tilbaketrekning",
|
||||||
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
"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",
|
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
||||||
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
||||||
"logRetentionRequestLabel": "Be om loggoverføring",
|
"logRetentionRequestLabel": "Be om loggbevaring",
|
||||||
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
||||||
"logRetentionAccessLabel": "Få tilgang til loggoverføring",
|
"logRetentionAccessLabel": "Få tilgang til loggoverføring",
|
||||||
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
|
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
|
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
|
||||||
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
|
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
|
||||||
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
|
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
|
||||||
"httpDestRequestLogsTitle": "Forespørselslogger (Automatic Translation)",
|
"httpDestRequestLogsTitle": "HTTP-forespørselslogger",
|
||||||
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
|
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
|
||||||
"httpDestSaveChanges": "Lagre endringer",
|
"httpDestSaveChanges": "Lagre endringer",
|
||||||
"httpDestCreateDestination": "Opprett mål",
|
"httpDestCreateDestination": "Opprett mål",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
|
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
|
||||||
"domainPickerWildcardCertWarningLink": "Lær mer",
|
"domainPickerWildcardCertWarningLink": "Lær mer",
|
||||||
"health": "Helse",
|
"health": "Helse",
|
||||||
"domainPendingErrorTitle": "Verifiseringsproblem"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP-adres",
|
"ip": "IP-adres",
|
||||||
"reason": "Reden",
|
"reason": "Reden",
|
||||||
"requestLogs": "Logboeken aanvragen",
|
"requestLogs": "HTTP-aanvraaglogboeken",
|
||||||
"requestAnalytics": "Analytics opvragen",
|
"requestAnalytics": "Analytics opvragen",
|
||||||
"host": "Hostnaam",
|
"host": "Hostnaam",
|
||||||
"location": "Locatie",
|
"location": "Locatie",
|
||||||
"actionLogs": "Actie logs",
|
"actionLogs": "Actie logs",
|
||||||
"sidebarLogsRequest": "Logboeken aanvragen",
|
"sidebarLogsRequest": "HTTP-aanvraaglogboeken",
|
||||||
"sidebarLogsAccess": "Toegang tot logboek",
|
"sidebarLogsAccess": "Toegang tot logboek",
|
||||||
"sidebarLogsAction": "Actie logs",
|
"sidebarLogsAction": "Actie logs",
|
||||||
"logRetention": "Log bewaring",
|
"logRetention": "Log bewaring",
|
||||||
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
"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",
|
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
||||||
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
||||||
"logRetentionRequestLabel": "Logboekbewaring aanvragen",
|
"logRetentionRequestLabel": "Bewaring van HTTP-aanvraaglogboeken",
|
||||||
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
||||||
"logRetentionAccessLabel": "Toegang logboek bewaring",
|
"logRetentionAccessLabel": "Toegang logboek bewaring",
|
||||||
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
|
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
|
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
|
||||||
"httpDestConnectionLogsTitle": "Connectie Logs",
|
"httpDestConnectionLogsTitle": "Connectie Logs",
|
||||||
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
|
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
|
||||||
"httpDestRequestLogsTitle": "Logboeken aanvragen",
|
"httpDestRequestLogsTitle": "HTTP-aanvraaglogboeken",
|
||||||
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
|
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
|
||||||
"httpDestSaveChanges": "Wijzigingen opslaan",
|
"httpDestSaveChanges": "Wijzigingen opslaan",
|
||||||
"httpDestCreateDestination": "Maak bestemming aan",
|
"httpDestCreateDestination": "Maak bestemming aan",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
|
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
|
||||||
"domainPickerWildcardCertWarningLink": "Meer informatie",
|
"domainPickerWildcardCertWarningLink": "Meer informatie",
|
||||||
"health": "Gezondheid",
|
"health": "Gezondheid",
|
||||||
"domainPendingErrorTitle": "Verificatieprobleem"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Powód",
|
"reason": "Powód",
|
||||||
"requestLogs": "Dzienniki żądań",
|
"requestLogs": "Dzienniki żądań HTTP",
|
||||||
"requestAnalytics": "Żądanie Analityki",
|
"requestAnalytics": "Żądanie Analityki",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Lokalizacja",
|
"location": "Lokalizacja",
|
||||||
"actionLogs": "Dzienniki działań",
|
"actionLogs": "Dzienniki działań",
|
||||||
"sidebarLogsRequest": "Dzienniki żądań",
|
"sidebarLogsRequest": "Dzienniki żądań HTTP",
|
||||||
"sidebarLogsAccess": "Logi dostępu",
|
"sidebarLogsAccess": "Logi dostępu",
|
||||||
"sidebarLogsAction": "Dzienniki działań",
|
"sidebarLogsAction": "Dzienniki działań",
|
||||||
"logRetention": "Zachowanie dziennika",
|
"logRetention": "Zachowanie dziennika",
|
||||||
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
"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",
|
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
||||||
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
||||||
"logRetentionRequestLabel": "Zachowanie dziennika żądań",
|
"logRetentionRequestLabel": "Przechowywanie dzienników żądań HTTP",
|
||||||
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
||||||
"logRetentionAccessLabel": "Zachowanie dziennika dostępu",
|
"logRetentionAccessLabel": "Zachowanie dziennika dostępu",
|
||||||
"logRetentionAccessDescription": "Jak długo zachować dzienniki 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.",
|
"httpDestActionLogsDescription": "Działania administracyjne wykonywane przez użytkowników w organizacji.",
|
||||||
"httpDestConnectionLogsTitle": "Dzienniki połączeń",
|
"httpDestConnectionLogsTitle": "Dzienniki połączeń",
|
||||||
"httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.",
|
"httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.",
|
||||||
"httpDestRequestLogsTitle": "Dzienniki żądań",
|
"httpDestRequestLogsTitle": "Dzienniki żądań HTTP",
|
||||||
"httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.",
|
"httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.",
|
||||||
"httpDestSaveChanges": "Zapisz zmiany",
|
"httpDestSaveChanges": "Zapisz zmiany",
|
||||||
"httpDestCreateDestination": "Utwórz cel",
|
"httpDestCreateDestination": "Utwórz cel",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
||||||
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
||||||
"health": "Zdrowie",
|
"health": "Zdrowie",
|
||||||
"domainPendingErrorTitle": "Problem z weryfikacją"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "PI",
|
"ip": "PI",
|
||||||
"reason": "Motivo",
|
"reason": "Motivo",
|
||||||
"requestLogs": "Registro de pedidos",
|
"requestLogs": "Registros de Pedidos HTTP",
|
||||||
"requestAnalytics": "Solicitar análise",
|
"requestAnalytics": "Solicitar análise",
|
||||||
"host": "Servidor",
|
"host": "Servidor",
|
||||||
"location": "Local:",
|
"location": "Local:",
|
||||||
"actionLogs": "Logs de Ações",
|
"actionLogs": "Logs de Ações",
|
||||||
"sidebarLogsRequest": "Registro de pedidos",
|
"sidebarLogsRequest": "Registros de Pedidos HTTP",
|
||||||
"sidebarLogsAccess": "Logs de Acesso",
|
"sidebarLogsAccess": "Logs de Acesso",
|
||||||
"sidebarLogsAction": "Logs de Ações",
|
"sidebarLogsAction": "Logs de Ações",
|
||||||
"logRetention": "Retenção de Log",
|
"logRetention": "Retenção de Log",
|
||||||
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
"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",
|
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
||||||
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
||||||
"logRetentionRequestLabel": "Solicitar retenção de registro",
|
"logRetentionRequestLabel": "Retenção de Registro de Pedido HTTP",
|
||||||
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
||||||
"logRetentionAccessLabel": "Retenção de Log de Acesso",
|
"logRetentionAccessLabel": "Retenção de Log de Acesso",
|
||||||
"logRetentionAccessDescription": "Por quanto tempo manter os registros 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.",
|
"httpDestActionLogsDescription": "Ações administrativas realizadas por usuários dentro da organização.",
|
||||||
"httpDestConnectionLogsTitle": "Logs da conexão",
|
"httpDestConnectionLogsTitle": "Logs da conexão",
|
||||||
"httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.",
|
"httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.",
|
||||||
"httpDestRequestLogsTitle": "Registro de pedidos",
|
"httpDestRequestLogsTitle": "Registros de Pedidos HTTP",
|
||||||
"httpDestRequestLogsDescription": "Logs de solicitação HTTP para recursos proxy incluindo o método, o caminho e o código de resposta.",
|
"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",
|
"httpDestSaveChanges": "Salvar as alterações",
|
||||||
"httpDestCreateDestination": "Criar destino",
|
"httpDestCreateDestination": "Criar destino",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
|
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Saiba mais",
|
"domainPickerWildcardCertWarningLink": "Saiba mais",
|
||||||
"health": "Saúde",
|
"health": "Saúde",
|
||||||
"domainPendingErrorTitle": "Problema de Verificação"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Причина",
|
"reason": "Причина",
|
||||||
"requestLogs": "Запросить журналы",
|
"requestLogs": "HTTP Запросы Логи",
|
||||||
"requestAnalytics": "Аналитика запроса",
|
"requestAnalytics": "Аналитика запроса",
|
||||||
"host": "Хост",
|
"host": "Хост",
|
||||||
"location": "Местоположение",
|
"location": "Местоположение",
|
||||||
"actionLogs": "Журнал действий",
|
"actionLogs": "Журнал действий",
|
||||||
"sidebarLogsRequest": "Запросить журналы",
|
"sidebarLogsRequest": "HTTP Запросы Логи",
|
||||||
"sidebarLogsAccess": "Журналы доступа",
|
"sidebarLogsAccess": "Журналы доступа",
|
||||||
"sidebarLogsAction": "Журнал действий",
|
"sidebarLogsAction": "Журнал действий",
|
||||||
"logRetention": "Сохранение журнала",
|
"logRetention": "Сохранение журнала",
|
||||||
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
||||||
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
||||||
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
||||||
"logRetentionRequestLabel": "Запросить сохранение журнала",
|
"logRetentionRequestLabel": "Сохранение HTTP Запросов Лога",
|
||||||
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
||||||
"logRetentionAccessLabel": "Хранение журнала доступа",
|
"logRetentionAccessLabel": "Хранение журнала доступа",
|
||||||
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
|
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
||||||
"httpDestConnectionLogsTitle": "Журнал подключений",
|
"httpDestConnectionLogsTitle": "Журнал подключений",
|
||||||
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
||||||
"httpDestRequestLogsTitle": "Запросить журналы",
|
"httpDestRequestLogsTitle": "HTTP Запросы Логи",
|
||||||
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
||||||
"httpDestSaveChanges": "Сохранить изменения",
|
"httpDestSaveChanges": "Сохранить изменения",
|
||||||
"httpDestCreateDestination": "Создать адрес назначения",
|
"httpDestCreateDestination": "Создать адрес назначения",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
|
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
|
||||||
"domainPickerWildcardCertWarningLink": "Узнать больше",
|
"domainPickerWildcardCertWarningLink": "Узнать больше",
|
||||||
"health": "Состояние",
|
"health": "Состояние",
|
||||||
"domainPendingErrorTitle": "Проблема с подтверждением"
|
"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": "Следующий"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Sebep",
|
"reason": "Sebep",
|
||||||
"requestLogs": "İstek Günlükleri",
|
"requestLogs": "HTTP İstek Günlükleri",
|
||||||
"requestAnalytics": "İstek Analizi",
|
"requestAnalytics": "İstek Analizi",
|
||||||
"host": "Sunucu",
|
"host": "Sunucu",
|
||||||
"location": "Konum",
|
"location": "Konum",
|
||||||
"actionLogs": "Eylem Günlükleri",
|
"actionLogs": "Eylem Günlükleri",
|
||||||
"sidebarLogsRequest": "İstek Günlükleri",
|
"sidebarLogsRequest": "HTTP İstek Günlükleri",
|
||||||
"sidebarLogsAccess": "Erişim Günlükleri",
|
"sidebarLogsAccess": "Erişim Günlükleri",
|
||||||
"sidebarLogsAction": "Eylem Günlükleri",
|
"sidebarLogsAction": "Eylem Günlükleri",
|
||||||
"logRetention": "Kayıt Saklama",
|
"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",
|
"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",
|
"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.",
|
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
||||||
"logRetentionRequestLabel": "İstek Günlüğü Saklama",
|
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
|
||||||
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
||||||
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
||||||
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
|
"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.",
|
"httpDestActionLogsDescription": "Kullanıcılar tarafından organizasyon içerisinde yapılan yönetici eylemleri.",
|
||||||
"httpDestConnectionLogsTitle": "Bağlantı Kayıtları",
|
"httpDestConnectionLogsTitle": "Bağlantı Kayıtları",
|
||||||
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
|
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
|
||||||
"httpDestRequestLogsTitle": "İstek Kayıtları",
|
"httpDestRequestLogsTitle": "HTTP İstek Günlükleri",
|
||||||
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
|
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
|
||||||
"httpDestSaveChanges": "Değişiklikleri Kaydet",
|
"httpDestSaveChanges": "Değişiklikleri Kaydet",
|
||||||
"httpDestCreateDestination": "Hedef Oluştur",
|
"httpDestCreateDestination": "Hedef Oluştur",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
|
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
|
||||||
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
|
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
|
||||||
"health": "Sağlık",
|
"health": "Sağlık",
|
||||||
"domainPendingErrorTitle": "Doğrulama Sorunu"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2672,7 +2672,7 @@
|
|||||||
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
||||||
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
||||||
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
||||||
"logRetentionRequestLabel": "请求日志保留",
|
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
||||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||||
"logRetentionAccessLabel": "访问日志保留",
|
"logRetentionAccessLabel": "访问日志保留",
|
||||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||||
@@ -3208,5 +3208,48 @@
|
|||||||
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
|
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
|
||||||
"domainPickerWildcardCertWarningLink": "了解更多",
|
"domainPickerWildcardCertWarningLink": "了解更多",
|
||||||
"health": "健康",
|
"health": "健康",
|
||||||
"domainPendingErrorTitle": "验证问题"
|
"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": "下一页"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { and, eq, inArray } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
createOrgUser = "createOrgUser",
|
createOrgUser = "createOrgUser",
|
||||||
@@ -153,21 +152,7 @@ export enum ActionsEnum {
|
|||||||
createHealthCheck = "createHealthCheck",
|
createHealthCheck = "createHealthCheck",
|
||||||
updateHealthCheck = "updateHealthCheck",
|
updateHealthCheck = "updateHealthCheck",
|
||||||
deleteHealthCheck = "deleteHealthCheck",
|
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(
|
export async function checkUserActionPermission(
|
||||||
@@ -200,23 +185,6 @@ 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
|
// Check if the user has direct permission for the action in the current org
|
||||||
const userActionPermission = await db
|
const userActionPermission = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -234,7 +202,20 @@ export async function checkUserActionPermission(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// 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;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking user action permission:", error);
|
console.error("Error checking user action permission:", error);
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import {
|
import { clients, db, resources, siteResources } from "@server/db";
|
||||||
clients,
|
|
||||||
db,
|
|
||||||
resourcePolicies,
|
|
||||||
resources,
|
|
||||||
siteResources
|
|
||||||
} from "@server/db";
|
|
||||||
import { randomInt } from "crypto";
|
import { randomInt } from "crypto";
|
||||||
import { exitNodes, sites } from "@server/db";
|
import { exitNodes, sites } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -113,35 +107,6 @@ 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(
|
export async function getUniqueSiteResourceName(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function createDb() {
|
|||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
export const primaryDb = db.$primary;
|
export const primaryDb = db.$primary as typeof db; // is this typeof a problem - techincally they are different types
|
||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -110,16 +110,6 @@ export const sites = pgTable("sites", {
|
|||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
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 })
|
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||||
.unique()
|
.unique()
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -206,11 +196,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}).notNull(),
|
||||||
.notNull(),
|
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
@@ -533,38 +521,6 @@ export const userResources = pgTable("userResources", {
|
|||||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
.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", {
|
export const userInvites = pgTable("userInvites", {
|
||||||
inviteId: varchar("inviteId").primaryKey(),
|
inviteId: varchar("inviteId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -630,40 +586,6 @@ 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", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -757,43 +679,6 @@ export const resourceRules = pgTable("resourceRules", {
|
|||||||
value: varchar("value").notNull()
|
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", {
|
export const supporterKey = pgTable("supporterKey", {
|
||||||
keyId: serial("keyId").primaryKey(),
|
keyId: serial("keyId").primaryKey(),
|
||||||
key: varchar("key").notNull(),
|
key: varchar("key").notNull(),
|
||||||
@@ -1212,9 +1097,7 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
|||||||
complete: boolean("complete").notNull().default(false)
|
complete: boolean("complete").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const statusHistory = pgTable(
|
export const statusHistory = pgTable("statusHistory", {
|
||||||
"statusHistory",
|
|
||||||
{
|
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
entityType: varchar("entityType").notNull(),
|
entityType: varchar("entityType").notNull(),
|
||||||
entityId: integer("entityId").notNull(),
|
entityId: integer("entityId").notNull(),
|
||||||
@@ -1222,20 +1105,11 @@ export const statusHistory = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
status: varchar("status").notNull(),
|
status: varchar("status").notNull(),
|
||||||
timestamp: integer("timestamp").notNull()
|
timestamp: integer("timestamp").notNull(),
|
||||||
},
|
}, (table) => [
|
||||||
(table) => [
|
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||||
index("idx_statusHistory_entity").on(
|
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||||
table.entityType,
|
]);
|
||||||
table.entityId,
|
|
||||||
table.timestamp
|
|
||||||
),
|
|
||||||
index("idx_statusHistory_org_timestamp").on(
|
|
||||||
table.orgId,
|
|
||||||
table.timestamp
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
@@ -1305,6 +1179,3 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type Network = InferSelectModel<typeof networks>;
|
export type Network = InferSelectModel<typeof networks>;
|
||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
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,13 +17,10 @@ import {
|
|||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
ResourceHeaderAuth,
|
ResourceHeaderAuth,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourcePolicyRules,
|
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
rolePolicies,
|
|
||||||
sessions,
|
sessions,
|
||||||
userResources,
|
userResources,
|
||||||
userPolicies,
|
|
||||||
users,
|
users,
|
||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
resourceHeaderAuthExtendedCompatibility
|
resourceHeaderAuthExtendedCompatibility
|
||||||
@@ -157,14 +154,13 @@ export async function getRoleName(roleId: number): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if role has access to resource (direct or via resource policy)
|
* Check if role has access to resource
|
||||||
*/
|
*/
|
||||||
export async function getRoleResourceAccess(
|
export async function getRoleResourceAccess(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleIds: number[]
|
roleIds: number[]
|
||||||
) {
|
) {
|
||||||
const [direct, viaPolicies] = await Promise.all([
|
const roleResourceAccess = await db
|
||||||
db
|
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
.where(
|
.where(
|
||||||
@@ -172,38 +168,19 @@ export async function getRoleResourceAccess(
|
|||||||
eq(roleResources.resourceId, resourceId),
|
eq(roleResources.resourceId, resourceId),
|
||||||
inArray(roleResources.roleId, roleIds)
|
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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const combined = [...direct, ...viaPolicies];
|
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||||
return combined.length > 0 ? combined : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has access to resource (direct or via resource policy)
|
* Check if user has direct access to resource
|
||||||
*/
|
*/
|
||||||
export async function getUserResourceAccess(
|
export async function getUserResourceAccess(
|
||||||
userId: string,
|
userId: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
) {
|
) {
|
||||||
const [direct, viaPolicies] = await Promise.all([
|
const userResourceAccess = await db
|
||||||
db
|
|
||||||
.select()
|
.select()
|
||||||
.from(userResources)
|
.from(userResources)
|
||||||
.where(
|
.where(
|
||||||
@@ -212,71 +189,23 @@ export async function getUserResourceAccess(
|
|||||||
eq(userResources.resourceId, resourceId)
|
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 direct[0] ?? viaPolicies[0] ?? null;
|
return userResourceAccess.length > 0 ? userResourceAccess[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get resource rules for a given resource (direct and via resource policy)
|
* Get resource rules for a given resource
|
||||||
*/
|
*/
|
||||||
export async function getResourceRules(
|
export async function getResourceRules(
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceRule[]> {
|
): Promise<ResourceRule[]> {
|
||||||
const [directRules, policyRules] = await Promise.all([
|
const rules = await db
|
||||||
db
|
|
||||||
.select()
|
.select()
|
||||||
.from(resourceRules)
|
.from(resourceRules)
|
||||||
.where(eq(resourceRules.resourceId, resourceId)),
|
.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(
|
return rules;
|
||||||
(max, r) => Math.max(max, r.priority),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const offsetPolicyRules = policyRules.map((r) => ({
|
|
||||||
...r,
|
|
||||||
priority: maxDirectPriority + r.priority
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -121,16 +121,6 @@ export const sites = sqliteTable("sites", {
|
|||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
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 })
|
resourceGuid: text("resourceGuid", { length: 36 })
|
||||||
.unique()
|
.unique()
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -229,11 +219,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}).notNull(),
|
||||||
.notNull(),
|
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -921,47 +909,6 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: text("headerAuthHash").notNull()
|
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(
|
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||||
"resourceHeaderAuthExtendedCompatibility",
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
{
|
{
|
||||||
@@ -1076,77 +1023,6 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
value: text("value").notNull()
|
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", {
|
export const supporterKey = sqliteTable("supporterKey", {
|
||||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
@@ -1320,9 +1196,7 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
|||||||
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const statusHistory = sqliteTable(
|
export const statusHistory = sqliteTable("statusHistory", {
|
||||||
"statusHistory",
|
|
||||||
{
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
||||||
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
||||||
@@ -1330,20 +1204,11 @@ export const statusHistory = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
||||||
timestamp: integer("timestamp").notNull() // unix epoch seconds
|
timestamp: integer("timestamp").notNull(), // unix epoch seconds
|
||||||
},
|
}, (table) => [
|
||||||
(table) => [
|
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||||
index("idx_statusHistory_entity").on(
|
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||||
table.entityType,
|
]);
|
||||||
table.entityId,
|
|
||||||
table.timestamp
|
|
||||||
),
|
|
||||||
index("idx_statusHistory_org_timestamp").on(
|
|
||||||
table.orgId,
|
|
||||||
table.timestamp
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
@@ -1413,6 +1278,3 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
typeof roundTripMessageTracker
|
typeof roundTripMessageTracker
|
||||||
>;
|
>;
|
||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
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,8 +24,7 @@ export enum TierFeature {
|
|||||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain"
|
||||||
ResourcePolicies = "resourcePolicies"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -67,6 +66,5 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AlertingRules]: ["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 {
|
} else {
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainInfo:
|
let domainInfo:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -162,10 +162,9 @@ export const HeaderSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Schema for individual resource
|
// Schema for individual resource
|
||||||
export const PublicResourceSchema = z
|
export const ResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
policy: z.string().optional(),
|
|
||||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
@@ -341,8 +340,7 @@ export const PublicResourceSchema = z
|
|||||||
if (parts.includes("*", 1)) return false; // no further wildcards
|
if (parts.includes("*", 1)) return false; // no further wildcards
|
||||||
if (parts.length < 3) return false; // need at least *.label.tld
|
if (parts.length < 3) return false; // need at least *.label.tld
|
||||||
|
|
||||||
const labelRegex =
|
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||||
/^[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));
|
return parts.slice(1).every((label) => labelRegex.test(label));
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -356,7 +354,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
|||||||
return Object.keys(resource).length === 1 && resource.targets;
|
return Object.keys(resource).length === 1 && resource.targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivateResourceSchema = z
|
export const ClientResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http"]),
|
mode: z.enum(["host", "cidr", "http"]),
|
||||||
@@ -437,19 +435,19 @@ export const PrivateResourceSchema = z
|
|||||||
export const ConfigSchema = z
|
export const ConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
"proxy-resources": z
|
"proxy-resources": z
|
||||||
.record(z.string(), PublicResourceSchema)
|
.record(z.string(), ResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"public-resources": z
|
"public-resources": z
|
||||||
.record(z.string(), PublicResourceSchema)
|
.record(z.string(), ResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"client-resources": z
|
"client-resources": z
|
||||||
.record(z.string(), PrivateResourceSchema)
|
.record(z.string(), ClientResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"private-resources": z
|
"private-resources": z
|
||||||
.record(z.string(), PrivateResourceSchema)
|
.record(z.string(), ClientResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||||
@@ -474,13 +472,10 @@ export const ConfigSchema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data as {
|
return data as {
|
||||||
"proxy-resources": Record<
|
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
|
||||||
string,
|
|
||||||
z.infer<typeof PublicResourceSchema>
|
|
||||||
>;
|
|
||||||
"client-resources": Record<
|
"client-resources": Record<
|
||||||
string,
|
string,
|
||||||
z.infer<typeof PrivateResourceSchema>
|
z.infer<typeof ClientResourceSchema>
|
||||||
>;
|
>;
|
||||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||||
};
|
};
|
||||||
@@ -619,5 +614,5 @@ export const ConfigSchema = z
|
|||||||
// Type inference from the schema
|
// Type inference from the schema
|
||||||
export type Site = z.infer<typeof SiteSchema>;
|
export type Site = z.infer<typeof SiteSchema>;
|
||||||
export type Target = z.infer<typeof TargetSchema>;
|
export type Target = z.infer<typeof TargetSchema>;
|
||||||
export type Resource = z.infer<typeof PublicResourceSchema>;
|
export type Resource = z.infer<typeof ResourceSchema>;
|
||||||
export type Config = z.infer<typeof ConfigSchema>;
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import { tierMatrix } from "./billing/tierMatrix";
|
|||||||
|
|
||||||
export async function calculateUserClientsForOrgs(
|
export async function calculateUserClientsForOrgs(
|
||||||
userId: string,
|
userId: string,
|
||||||
trx?: Transaction
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const execute = async (transaction: Transaction) => {
|
const execute = async (transaction: Transaction | typeof db) => {
|
||||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||||
const adminRoleCache = new Map<
|
const adminRoleCache = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -437,7 +437,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
|
|
||||||
async function cleanupOrphanedClients(
|
async function cleanupOrphanedClients(
|
||||||
userId: string,
|
userId: string,
|
||||||
trx: Transaction,
|
trx: Transaction | typeof db,
|
||||||
userOrgIds: string[] = []
|
userOrgIds: string[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Find all OLM clients for this user that should be deleted
|
// Find all OLM clients for this user that should be deleted
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.18.2";
|
export const APP_VERSION = "1.18.3";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import z from "zod";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||||
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
start: bigint;
|
start: bigint;
|
||||||
@@ -327,6 +328,9 @@ export async function getNextAvailableClientSubnet(
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
transaction: Transaction | typeof db = db
|
transaction: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
return await lockManager.withLock(
|
||||||
|
`client-subnet-allocation:${orgId}`,
|
||||||
|
async () => {
|
||||||
const [org] = await transaction
|
const [org] = await transaction
|
||||||
.select()
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
@@ -337,7 +341,9 @@ export async function getNextAvailableClientSubnet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org.subnet) {
|
||||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no subnet defined`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAddressesSites = await transaction
|
const existingAddressesSites = await transaction
|
||||||
@@ -352,7 +358,9 @@ export async function getNextAvailableClientSubnet(
|
|||||||
address: clients.subnet
|
address: clients.subnet
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
|
.where(
|
||||||
|
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
|
||||||
|
);
|
||||||
|
|
||||||
const addresses = [
|
const addresses = [
|
||||||
...existingAddressesSites.map(
|
...existingAddressesSites.map(
|
||||||
@@ -370,18 +378,29 @@ export async function getNextAvailableClientSubnet(
|
|||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNextAvailableAliasAddress(
|
export async function getNextAvailableAliasAddress(
|
||||||
orgId: string
|
orgId: string,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
return await lockManager.withLock(
|
||||||
|
`alias-address-allocation:${orgId}`,
|
||||||
|
async () => {
|
||||||
|
const [org] = await trx
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new Error(`Organization with ID ${orgId} not found`);
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org.subnet) {
|
||||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no subnet defined`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.utilitySubnet) {
|
if (!org.utilitySubnet) {
|
||||||
@@ -390,7 +409,7 @@ export async function getNextAvailableAliasAddress(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAddresses = await db
|
const existingAddresses = await trx
|
||||||
.select({
|
.select({
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
})
|
})
|
||||||
@@ -410,7 +429,11 @@ export async function getNextAvailableAliasAddress(
|
|||||||
`${org.utilitySubnet.split("/")[0]}/29`
|
`${org.utilitySubnet.split("/")[0]}/29`
|
||||||
].filter((address) => address !== null) as string[];
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
let subnet = findNextAvailableCidr(
|
||||||
|
addresses,
|
||||||
|
32,
|
||||||
|
org.utilitySubnet
|
||||||
|
);
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
@@ -420,8 +443,11 @@ export async function getNextAvailableAliasAddress(
|
|||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||||
|
return await lockManager.withLock("org-subnet-allocation", async () => {
|
||||||
const existingAddresses = await db
|
const existingAddresses = await db
|
||||||
.select({
|
.select({
|
||||||
subnet: orgs.subnet
|
subnet: orgs.subnet
|
||||||
@@ -441,6 +467,7 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRemoteSubnets(
|
export function generateRemoteSubnets(
|
||||||
@@ -478,7 +505,12 @@ export type Alias = { alias: string | null; aliasAddress: string | null };
|
|||||||
|
|
||||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||||
return allSiteResources
|
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) => ({
|
.map((sr) => ({
|
||||||
alias: sr.alias || sr.fullDomain,
|
alias: sr.alias || sr.fullDomain,
|
||||||
aliasAddress: sr.aliasAddress
|
aliasAddress: sr.aliasAddress
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ export async function getCachedStatusHistory(
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
// Anchor to UTC midnight so the query window aligns with stable calendar days
|
||||||
const startSec = nowSec - days * 86400;
|
const utcToday = new Date();
|
||||||
|
utcToday.setUTCHours(0, 0, 0, 0);
|
||||||
|
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
||||||
|
const startSec = todayMidnightSec - days * 86400;
|
||||||
|
|
||||||
const events = await logsDb
|
const events = await logsDb
|
||||||
.select()
|
.select()
|
||||||
@@ -110,11 +113,18 @@ export function computeBuckets(
|
|||||||
days: number
|
days: number
|
||||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
|
||||||
|
// 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[] = [];
|
const buckets: StatusHistoryDayBucket[] = [];
|
||||||
let totalDowntime = 0;
|
let totalDowntime = 0;
|
||||||
|
|
||||||
for (let d = 0; d < days; d++) {
|
for (let d = 0; d < days; d++) {
|
||||||
const dayStartSec = nowSec - (days - d) * 86400;
|
const dayStartSec = todayMidnightSec - (days - 1 - d) * 86400;
|
||||||
const dayEndSec = dayStartSec + 86400;
|
const dayEndSec = dayStartSec + 86400;
|
||||||
|
|
||||||
const dayEvents = events.filter(
|
const dayEvents = events.filter(
|
||||||
|
|||||||
@@ -32,4 +32,3 @@ export * from "./verifySiteResourceAccess";
|
|||||||
export * from "./logActionAudit";
|
export * from "./logActionAudit";
|
||||||
export * from "./verifyOlmAccess";
|
export * from "./verifyOlmAccess";
|
||||||
export * from "./verifyLimits";
|
export * from "./verifyLimits";
|
||||||
export * from "./verifyResourcePolicyAccess";
|
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ export * from "./verifyApiKeyClientAccess";
|
|||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
export * from "./verifyApiKeyDomainAccess";
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
export * from "./verifyApiKeyResourcePolicyAccess";
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have permission to set user organization roles"
|
"User does not have permission perform this action"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export enum OpenAPITags {
|
|||||||
Org = "Organization",
|
Org = "Organization",
|
||||||
PublicResource = "Public Resource",
|
PublicResource = "Public Resource",
|
||||||
PrivateResource = "Private Resource",
|
PrivateResource = "Private Resource",
|
||||||
Policy = "Policy",
|
|
||||||
Role = "Role",
|
Role = "Role",
|
||||||
User = "User",
|
User = "User",
|
||||||
Invitation = "User Invitation",
|
Invitation = "User Invitation",
|
||||||
|
|||||||
@@ -485,6 +485,133 @@ async function syncAcmeCertsFromHttp(endpoint: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function storeCertForDomain(
|
||||||
|
domain: string,
|
||||||
|
certPem: string,
|
||||||
|
keyPem: string,
|
||||||
|
validatedX509: crypto.X509Certificate
|
||||||
|
): Promise<void> {
|
||||||
|
const wildcard = domain.startsWith("*.");
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(certificates)
|
||||||
|
.where(eq(certificates.domain, domain))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let oldCertPem: string | null = null;
|
||||||
|
let oldKeyPem: string | null = null;
|
||||||
|
|
||||||
|
if (existing.length > 0 && existing[0].certFile) {
|
||||||
|
try {
|
||||||
|
const storedCertPem = decrypt(
|
||||||
|
existing[0].certFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
||||||
|
if (storedCertPem === certPem && wildcardUnchanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oldCertPem = storedCertPem;
|
||||||
|
if (existing[0].keyFile) {
|
||||||
|
try {
|
||||||
|
oldKeyPem = decrypt(
|
||||||
|
existing[0].keyFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
} catch (keyErr) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiresAt: number | null = null;
|
||||||
|
try {
|
||||||
|
expiresAt = Math.floor(
|
||||||
|
new Date(validatedX509.validTo).getTime() / 1000
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedCert = encrypt(
|
||||||
|
certPem,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const encryptedKey = encrypt(keyPem, config.getRawConfig().server.secret!);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const domainId = await findDomainId(domain);
|
||||||
|
if (domainId) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(certificates)
|
||||||
|
.set({
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard,
|
||||||
|
...(domainId !== null && { domainId })
|
||||||
|
})
|
||||||
|
.where(eq(certificates.domain, domain));
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
oldCertPem,
|
||||||
|
oldKeyPem
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db.insert(certificates).values({
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function findAcmeJsonFiles(dirPath: string): string[] {
|
function findAcmeJsonFiles(dirPath: string): string[] {
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
let entries: fs.Dirent[];
|
let entries: fs.Dirent[];
|
||||||
@@ -575,18 +702,16 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const cert of allCerts) {
|
for (const cert of allCerts) {
|
||||||
const domain = cert?.domain?.main;
|
const mainDomain = cert?.domain?.main;
|
||||||
|
|
||||||
if (!domain || typeof domain !== "string") {
|
if (!mainDomain || typeof mainDomain !== "string") {
|
||||||
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { wildcard } = detectWildcard(domain, cert.domain?.sans);
|
|
||||||
|
|
||||||
if (!cert.certificate || !cert.key) {
|
if (!cert.certificate || !cert.key) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - empty certificate or key field`
|
`acmeCertSync: skipping cert for ${mainDomain} - empty certificate or key field`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -598,14 +723,14 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - failed to base64-decode cert/key: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - failed to base64-decode cert/key: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!certPem.trim() || !keyPem.trim()) {
|
if (!certPem.trim() || !keyPem.trim()) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - blank PEM after base64 decode`
|
`acmeCertSync: skipping cert for ${mainDomain} - blank PEM after base64 decode`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -616,7 +741,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
const firstCertPemForValidation = extractFirstCert(certPem);
|
const firstCertPemForValidation = extractFirstCert(certPem);
|
||||||
if (!firstCertPemForValidation) {
|
if (!firstCertPemForValidation) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - no PEM certificate block found`
|
`acmeCertSync: skipping cert for ${mainDomain} - no PEM certificate block found`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -628,7 +753,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - invalid X.509 certificate: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid X.509 certificate: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -638,139 +763,40 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
crypto.createPrivateKey(keyPem);
|
crypto.createPrivateKey(keyPem);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - invalid private key: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid private key: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if cert already exists in DB
|
// Collect all domains covered by this cert: main + every SAN.
|
||||||
const existing = await db
|
// Each domain gets its own row in the certificates table so that
|
||||||
.select()
|
// lookups by any hostname on the cert succeed independently.
|
||||||
.from(certificates)
|
const allDomains = new Set<string>([mainDomain]);
|
||||||
.where(and(eq(certificates.domain, domain)))
|
if (Array.isArray(cert.domain?.sans)) {
|
||||||
.limit(1);
|
for (const san of cert.domain.sans) {
|
||||||
|
if (typeof san === "string" && san.trim()) {
|
||||||
let oldCertPem: string | null = null;
|
allDomains.add(san.trim());
|
||||||
let oldKeyPem: string | null = null;
|
|
||||||
|
|
||||||
if (existing.length > 0 && existing[0].certFile) {
|
|
||||||
try {
|
|
||||||
const storedCertPem = decrypt(
|
|
||||||
existing[0].certFile,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
|
||||||
if (storedCertPem === certPem && wildcardUnchanged) {
|
|
||||||
// logger.debug(
|
|
||||||
// `acmeCertSync: cert for ${domain} is unchanged, skipping`
|
|
||||||
// );
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
// Cert has changed; capture old values so we can send a correct
|
}
|
||||||
// update message to the newt after the DB write.
|
}
|
||||||
oldCertPem = storedCertPem;
|
|
||||||
if (existing[0].keyFile) {
|
|
||||||
try {
|
|
||||||
oldKeyPem = decrypt(
|
|
||||||
existing[0].keyFile,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
} catch (keyErr) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
`acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Decryption failure means we should proceed with the update
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse cert expiry from the validated X.509 certificate
|
for (const domain of allDomains) {
|
||||||
let expiresAt: number | null = null;
|
|
||||||
try {
|
try {
|
||||||
expiresAt = Math.floor(
|
await storeCertForDomain(
|
||||||
new Date(validatedX509.validTo).getTime() / 1000
|
domain,
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedCert = encrypt(
|
|
||||||
certPem,
|
certPem,
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
const encryptedKey = encrypt(
|
|
||||||
keyPem,
|
keyPem,
|
||||||
config.getRawConfig().server.secret!
|
validatedX509
|
||||||
);
|
);
|
||||||
const now = Math.floor(Date.now() / 1000);
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
const domainId = await findDomainId(domain);
|
`acmeCertSync: error storing cert for domain "${domain}": ${err}`
|
||||||
if (domainId) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing.length > 0) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.update(certificates)
|
|
||||||
.set({
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard,
|
|
||||||
...(domainId !== null && { domainId })
|
|
||||||
})
|
|
||||||
.where(eq(certificates.domain, domain));
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
|
|
||||||
await pushCertUpdateToAffectedNewts(
|
|
||||||
domain,
|
|
||||||
domainId,
|
|
||||||
oldCertPem,
|
|
||||||
oldKeyPem
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
await db.insert(certificates).values({
|
|
||||||
domain,
|
|
||||||
domainId,
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// For a brand-new cert, push to any SSL resources that were waiting for it
|
|
||||||
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ import { decrypt } from "@server/lib/crypto";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendAlertWebhook } from "./sendAlertWebhook";
|
import { sendAlertWebhook } from "./sendAlertWebhook";
|
||||||
import { sendAlertEmail } from "./sendAlertEmail";
|
import { sendAlertEmail } from "./sendAlertEmail";
|
||||||
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
import {
|
||||||
|
AlertContext,
|
||||||
|
WebhookAlertConfig
|
||||||
|
} from "@server/routers/alertRule/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core alert processing pipeline.
|
* Core alert processing pipeline.
|
||||||
@@ -99,7 +102,10 @@ export async function processAlerts(context: AlertContext): Promise<void> {
|
|||||||
baseConditions,
|
baseConditions,
|
||||||
or(
|
or(
|
||||||
eq(alertRules.allHealthChecks, true),
|
eq(alertRules.allHealthChecks, true),
|
||||||
eq(alertHealthChecks.healthCheckId, context.healthCheckId)
|
eq(
|
||||||
|
alertHealthChecks.healthCheckId,
|
||||||
|
context.healthCheckId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -208,14 +214,19 @@ async function processRule(
|
|||||||
|
|
||||||
for (const action of emailActions) {
|
for (const action of emailActions) {
|
||||||
try {
|
try {
|
||||||
const recipients = await resolveEmailRecipients(action.emailActionId);
|
const recipients = await resolveEmailRecipients(
|
||||||
|
action.emailActionId
|
||||||
|
);
|
||||||
if (recipients.length > 0) {
|
if (recipients.length > 0) {
|
||||||
await sendAlertEmail(recipients, context);
|
await sendAlertEmail(recipients, context);
|
||||||
await db
|
await db
|
||||||
.update(alertEmailActions)
|
.update(alertEmailActions)
|
||||||
.set({ lastSentAt: now })
|
.set({ lastSentAt: now })
|
||||||
.where(
|
.where(
|
||||||
eq(alertEmailActions.emailActionId, action.emailActionId)
|
eq(
|
||||||
|
alertEmailActions.emailActionId,
|
||||||
|
action.emailActionId
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -269,7 +280,7 @@ async function processRule(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.warn(
|
||||||
`processAlerts: failed to send alert webhook for action ${action.webhookActionId}`,
|
`processAlerts: failed to send alert webhook for action ${action.webhookActionId}`,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
@@ -289,7 +300,9 @@ async function processRule(
|
|||||||
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
||||||
* - Direct external email addresses
|
* - Direct external email addresses
|
||||||
*/
|
*/
|
||||||
async function resolveEmailRecipients(emailActionId: number): Promise<string[]> {
|
async function resolveEmailRecipients(
|
||||||
|
emailActionId: number
|
||||||
|
): Promise<string[]> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(alertEmailRecipients)
|
.from(alertEmailRecipients)
|
||||||
|
|||||||
@@ -236,15 +236,43 @@ interface TemplateContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a body template with {{event}}, {{timestamp}}, {{status}}, and
|
* Render a body template with {{event}}, {{timestamp}}, {{status}}, {{data}},
|
||||||
* {{data}} placeholders, mirroring the logic in HttpLogDestination.
|
* and individual data-field placeholders (e.g. {{orgId}}, {{siteId}}, …).
|
||||||
*
|
*
|
||||||
* {{data}} is replaced first (as raw JSON) so that any literal "{{…}}"
|
* Replacement order:
|
||||||
* strings inside data values are not re-expanded.
|
* 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.
|
||||||
*/
|
*/
|
||||||
function renderTemplate(template: string, ctx: TemplateContext): string {
|
function renderTemplate(template: string, ctx: TemplateContext): string {
|
||||||
const rendered = template
|
// Step 1 – expand {{data}} first so its contents are already serialised
|
||||||
.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data))
|
// 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
|
||||||
.replace(/\{\{event\}\}/g, escapeJsonString(ctx.event))
|
.replace(/\{\{event\}\}/g, escapeJsonString(ctx.event))
|
||||||
.replace(/\{\{timestamp\}\}/g, escapeJsonString(ctx.timestamp))
|
.replace(/\{\{timestamp\}\}/g, escapeJsonString(ctx.timestamp))
|
||||||
.replace(/\{\{status\}\}/g, escapeJsonString(ctx.status));
|
.replace(/\{\{status\}\}/g, escapeJsonString(ctx.status));
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
import * as resource from "#private/routers/resource";
|
|
||||||
import * as policy from "#private/routers/policy";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -46,8 +44,7 @@ import {
|
|||||||
verifyUserCanSetUserOrgRoles,
|
verifyUserCanSetUserOrgRoles,
|
||||||
verifySiteProvisioningKeyAccess,
|
verifySiteProvisioningKeyAccess,
|
||||||
verifyIsLoggedInUser,
|
verifyIsLoggedInUser,
|
||||||
verifyAdmin,
|
verifyAdmin
|
||||||
verifyResourcePolicyAccess
|
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import {
|
import {
|
||||||
@@ -385,39 +382,6 @@ authenticated.get(
|
|||||||
approval.countApprovals
|
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(
|
authenticated.put(
|
||||||
"/org/:orgId/approvals/:approvalId",
|
"/org/:orgId/approvals/:approvalId",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
|
|||||||
@@ -45,11 +45,8 @@ import {
|
|||||||
users,
|
users,
|
||||||
userOrgs,
|
userOrgs,
|
||||||
roleResources,
|
roleResources,
|
||||||
rolePolicies,
|
|
||||||
userResources,
|
userResources,
|
||||||
userPolicies,
|
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourcePolicyRules,
|
|
||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
roles
|
roles
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
@@ -433,10 +430,7 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Decrypt and save key file
|
// Decrypt and save key file
|
||||||
const decryptedKey = decrypt(
|
const decryptedKey = decrypt(cert.keyFile!, config.getRawConfig().server.secret!);
|
||||||
cert.keyFile!,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return only the certificate data without org information
|
// Return only the certificate data without org information
|
||||||
return {
|
return {
|
||||||
@@ -537,10 +531,7 @@ hybridRouter.get(
|
|||||||
wildcardCandidates.length > 0
|
wildcardCandidates.length > 0
|
||||||
? and(
|
? and(
|
||||||
eq(resources.wildcard, true),
|
eq(resources.wildcard, true),
|
||||||
inArray(
|
inArray(resources.fullDomain, wildcardCandidates)
|
||||||
resources.fullDomain,
|
|
||||||
wildcardCandidates
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
: sql`false`
|
: sql`false`
|
||||||
)
|
)
|
||||||
@@ -554,10 +545,10 @@ hybridRouter.get(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
result &&
|
result &&
|
||||||
(await checkExitNodeOrg(
|
await checkExitNodeOrg(
|
||||||
remoteExitNode.exitNodeId,
|
remoteExitNode.exitNodeId,
|
||||||
result.resources.orgId
|
result.resources.orgId
|
||||||
))
|
)
|
||||||
) {
|
) {
|
||||||
// If the exit node is not allowed for the org, return an error
|
// If the exit node is not allowed for the org, return an error
|
||||||
return next(
|
return next(
|
||||||
@@ -1141,8 +1132,7 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [direct, viaPolicies] = await Promise.all([
|
const roleResourceAccess = await db
|
||||||
db
|
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
.where(
|
.where(
|
||||||
@@ -1151,33 +1141,13 @@ hybridRouter.get(
|
|||||||
eq(roleResources.roleId, roleId)
|
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 = direct[0] ?? viaPolicies[0] ?? null;
|
const result =
|
||||||
|
roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
||||||
|
|
||||||
return response<typeof roleResources.$inferSelect | null>(res, {
|
return response<typeof roleResources.$inferSelect | null>(res, {
|
||||||
data: result as any,
|
data: result,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: result
|
message: result
|
||||||
@@ -1252,8 +1222,7 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [direct, viaPolicies] = await Promise.all([
|
const roleResourceAccess = await db
|
||||||
db
|
|
||||||
.select({
|
.select({
|
||||||
resourceId: roleResources.resourceId,
|
resourceId: roleResources.resourceId,
|
||||||
roleId: roleResources.roleId
|
roleId: roleResources.roleId
|
||||||
@@ -1264,32 +1233,10 @@ hybridRouter.get(
|
|||||||
eq(roleResources.resourceId, resourceId),
|
eq(roleResources.resourceId, resourceId),
|
||||||
inArray(roleResources.roleId, roleIds)
|
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 combined = [...direct, ...viaPolicies];
|
const result =
|
||||||
const result = combined.length > 0 ? combined : null;
|
roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||||
|
|
||||||
return response<{ resourceId: number; roleId: number }[] | null>(
|
return response<{ resourceId: number; roleId: number }[] | null>(
|
||||||
res,
|
res,
|
||||||
@@ -1450,45 +1397,10 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [directRules, policyRules] = await Promise.all([
|
const rules = await db
|
||||||
db
|
|
||||||
.select()
|
.select()
|
||||||
.from(resourceRules)
|
.from(resourceRules)
|
||||||
.where(eq(resourceRules.resourceId, resourceId)),
|
.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
|
// backward compatibility: COUNTRY -> GEOIP
|
||||||
// TODO: remove this after a few versions once all exit nodes are updated
|
// TODO: remove this after a few versions once all exit nodes are updated
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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";
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -122,8 +122,12 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newUserRole: { userId: string; orgId: string; roleId: number } | null =
|
let newUserRole: {
|
||||||
null;
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
roleId: number;
|
||||||
|
} | null = null;
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const inserted = await trx
|
const inserted = await trx
|
||||||
.insert(userOrgRoles)
|
.insert(userOrgRoles)
|
||||||
@@ -149,11 +153,19 @@ export async function addUserRole(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: newUserRole ?? { userId, orgId: role.orgId, roleId },
|
data: newUserRole ?? { userId, orgId: role.orgId, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -129,6 +129,7 @@ export async function removeUserRole(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -150,11 +151,19 @@ export async function removeUserRole(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { userId, orgId: role.orgId, roleId },
|
data: { userId, orgId: role.orgId, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -115,6 +115,7 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -142,11 +143,19 @@ export async function setUserOrgRoles(
|
|||||||
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
|
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { userId, orgId, roleIds: uniqueRoleIds },
|
data: { userId, orgId, roleIds: uniqueRoleIds },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, orgs, userOrgs, users } from "@server/db";
|
import { db, orgs, userOrgs, users, primaryDb } from "@server/db";
|
||||||
import { eq, and, inArray, not } from "drizzle-orm";
|
import { eq, and, inArray, not } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -218,13 +218,18 @@ export async function deleteMyAccount(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx.delete(users).where(eq(users.userId, userId));
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
|
||||||
// loop through the other orgs and decrement the count
|
// loop through the other orgs and decrement the count
|
||||||
for (const userOrg of otherOrgsTheUserWasIn) {
|
for (const userOrg of otherOrgsTheUserWasIn) {
|
||||||
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
|
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invalidateSession(session.sessionId);
|
await invalidateSession(session.sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -671,8 +671,7 @@ export async function verifyResourceSession(
|
|||||||
resourceData.org
|
resourceData.org
|
||||||
);
|
);
|
||||||
|
|
||||||
// this is query intensive so let it cache a little longer
|
localCache.set(userAccessCacheKey, allowedUserData, 5);
|
||||||
localCache.set(userAccessCacheKey, allowedUserData, 12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -1004,7 +1003,11 @@ async function checkRules(
|
|||||||
isIpInCidr(clientIp, rule.value)
|
isIpInCidr(clientIp, rule.value)
|
||||||
) {
|
) {
|
||||||
return rule.action as any;
|
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;
|
return rule.action as any;
|
||||||
} else if (
|
} else if (
|
||||||
path &&
|
path &&
|
||||||
@@ -1012,7 +1015,10 @@ async function checkRules(
|
|||||||
isPathAllowed(rule.value, path)
|
isPathAllowed(rule.value, path)
|
||||||
) {
|
) {
|
||||||
return rule.action as any;
|
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.
|
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
|
||||||
if (
|
if (
|
||||||
rule.value.toUpperCase() === "ALL" &&
|
rule.value.toUpperCase() === "ALL" &&
|
||||||
@@ -1024,7 +1030,10 @@ async function checkRules(
|
|||||||
if (await isIpInGeoIP(ipCC, rule.value)) {
|
if (await isIpInGeoIP(ipCC, rule.value)) {
|
||||||
return rule.action as any;
|
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.
|
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
|
||||||
if (
|
if (
|
||||||
(rule.value.toUpperCase() === "ALL" ||
|
(rule.value.toUpperCase() === "ALL" ||
|
||||||
@@ -1263,15 +1272,11 @@ export async function isIpInRegion(
|
|||||||
if (region.id === checkRegionCode) {
|
if (region.id === checkRegionCode) {
|
||||||
for (const subregion of region.includes) {
|
for (const subregion of region.includes) {
|
||||||
if (subregion.countries.includes(upperCode)) {
|
if (subregion.countries.includes(upperCode)) {
|
||||||
logger.debug(
|
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
|
||||||
`Country ${upperCode} is in region ${region.id} (${region.name})`
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(
|
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
|
||||||
`Country ${upperCode} is not in region ${region.id} (${region.name})`
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1279,14 +1284,10 @@ export async function isIpInRegion(
|
|||||||
for (const subregion of region.includes) {
|
for (const subregion of region.includes) {
|
||||||
if (subregion.id === checkRegionCode) {
|
if (subregion.id === checkRegionCode) {
|
||||||
if (subregion.countries.includes(upperCode)) {
|
if (subregion.countries.includes(upperCode)) {
|
||||||
logger.debug(
|
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
|
||||||
`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
logger.debug(
|
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
|
||||||
`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import {
|
import {
|
||||||
roles,
|
roles,
|
||||||
Client,
|
Client,
|
||||||
@@ -92,7 +92,10 @@ export async function createClient(
|
|||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
|
if (
|
||||||
|
req.user &&
|
||||||
|
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
|
||||||
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
@@ -198,7 +201,10 @@ export async function createClient(
|
|||||||
|
|
||||||
if (!randomExitNode) {
|
if (!randomExitNode) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +262,18 @@ export async function createClient(
|
|||||||
clientId: newClient.clientId,
|
clientId: newClient.clientId,
|
||||||
dateCreated: moment().toISOString()
|
dateCreated: moment().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newClient) {
|
||||||
|
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after creating client: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response<CreateClientResponse>(res, {
|
return response<CreateClientResponse>(res, {
|
||||||
data: newClient,
|
data: newClient,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import {
|
import {
|
||||||
roles,
|
roles,
|
||||||
Client,
|
Client,
|
||||||
@@ -237,10 +237,18 @@ export async function createUserClient(
|
|||||||
userId,
|
userId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newClient) {
|
||||||
|
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after creating user client: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response<CreateClientAndOlmResponse>(res, {
|
return response<CreateClientAndOlmResponse>(res, {
|
||||||
data: newClient,
|
data: newClient,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms, primaryDb, Client, Olm } from "@server/db";
|
||||||
import { clients, clientSitesAssociationsCache } from "@server/db";
|
import { clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -71,14 +71,17 @@ export async function deleteClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deletedClient: Client | undefined;
|
||||||
|
let olm: Olm | undefined;
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Then delete the client itself
|
// Then delete the client itself
|
||||||
const [deletedClient] = await trx
|
[deletedClient] = await trx
|
||||||
.delete(clients)
|
.delete(clients)
|
||||||
.where(eq(clients.clientId, clientId))
|
.where(eq(clients.clientId, clientId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const [olm] = await trx
|
[olm] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(olms)
|
.from(olms)
|
||||||
.where(eq(olms.clientId, clientId))
|
.where(eq(olms.clientId, clientId))
|
||||||
@@ -88,14 +91,29 @@ export async function deleteClient(
|
|||||||
if (!client.userId && client.olmId) {
|
if (!client.userId && client.olmId) {
|
||||||
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
|
||||||
|
|
||||||
if (olm) {
|
|
||||||
await sendTerminateClient(deletedClient.clientId, OlmErrorCodes.TERMINATED_DELETED, olm.olmId); // the olmId needs to be provided because it cant look it up after deletion
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (deletedClient) {
|
||||||
|
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (olm) {
|
||||||
|
sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
OlmErrorCodes.TERMINATED_DELETED,
|
||||||
|
olm.olmId
|
||||||
|
).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import config from "@server/lib/config";
|
|||||||
import * as site from "./site";
|
import * as site from "./site";
|
||||||
import * as org from "./org";
|
import * as org from "./org";
|
||||||
import * as resource from "./resource";
|
import * as resource from "./resource";
|
||||||
import * as policy from "./policy";
|
|
||||||
import * as domain from "./domain";
|
import * as domain from "./domain";
|
||||||
import * as target from "./target";
|
import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
@@ -43,8 +42,7 @@ import {
|
|||||||
verifyUserIsOrgOwner,
|
verifyUserIsOrgOwner,
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
verifyLimits,
|
verifyLimits
|
||||||
verifyResourcePolicyAccess
|
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
@@ -105,6 +103,7 @@ authenticated.put(
|
|||||||
site.createSite
|
site.createSite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/sites",
|
"/org/:orgId/sites",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -541,7 +540,6 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.getResource),
|
verifyUserHasAction(ActionsEnum.getResource),
|
||||||
resource.getResource
|
resource.getResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -648,29 +646,6 @@ authenticated.post(
|
|||||||
logActionAudit(ActionsEnum.updateRole),
|
logActionAudit(ActionsEnum.updateRole),
|
||||||
role.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(
|
// authenticated.get(
|
||||||
// "/role/:roleId",
|
// "/role/:roleId",
|
||||||
// verifyRoleAccess,
|
// verifyRoleAccess,
|
||||||
@@ -722,59 +697,6 @@ authenticated.post(
|
|||||||
resource.setResourceUsers
|
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(
|
authenticated.post(
|
||||||
`/resource/:resourceId/password`,
|
`/resource/:resourceId/password`,
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as site from "./site";
|
|||||||
import * as org from "./org";
|
import * as org from "./org";
|
||||||
import * as blueprints from "./blueprints";
|
import * as blueprints from "./blueprints";
|
||||||
import * as resource from "./resource";
|
import * as resource from "./resource";
|
||||||
import * as policy from "./policy";
|
|
||||||
import * as domain from "./domain";
|
import * as domain from "./domain";
|
||||||
import * as target from "./target";
|
import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
@@ -30,9 +29,7 @@ import {
|
|||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeySetResourceClients,
|
verifyApiKeySetResourceClients,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyApiKeyDomainAccess,
|
verifyApiKeyDomainAccess
|
||||||
verifyApiKeyResourcePolicyAccess,
|
|
||||||
verifyUserHasAction
|
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -462,20 +459,6 @@ authenticated.get(
|
|||||||
resource.getResource
|
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(
|
authenticated.post(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
@@ -485,13 +468,6 @@ authenticated.post(
|
|||||||
resource.updateResource
|
resource.updateResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/resource-policy/:resourcePolicyId",
|
|
||||||
verifyApiKeyResourcePolicyAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
|
|
||||||
policy.updateResourcePolicy
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
@@ -643,63 +619,6 @@ authenticated.post(
|
|||||||
resource.setResourceUsers
|
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(
|
authenticated.post(
|
||||||
"/resource/:resourceId/roles/add",
|
"/resource/:resourceId/roles/add",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms, primaryDb } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -81,8 +81,7 @@ export async function createUserOlm(
|
|||||||
|
|
||||||
const secretHash = await hashPassword(secret);
|
const secretHash = await hashPassword(secret);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.insert(olms).values({
|
||||||
await trx.insert(olms).values({
|
|
||||||
olmId: olmId,
|
olmId: olmId,
|
||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
@@ -90,7 +89,11 @@ export async function createUserOlm(
|
|||||||
dateCreated: moment().toISOString()
|
dateCreated: moment().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
console.error(
|
||||||
|
"Error calculating user clients after creating olm:",
|
||||||
|
e
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<CreateOlmResponse>(res, {
|
return response<CreateOlmResponse>(res, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { Client, db } from "@server/db";
|
import { Client, db, Olm, primaryDb } from "@server/db";
|
||||||
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -49,6 +49,7 @@ export async function deleteUserOlm(
|
|||||||
|
|
||||||
const { olmId } = parsedParams.data;
|
const { olmId } = parsedParams.data;
|
||||||
|
|
||||||
|
let deletedClient: Client | undefined;
|
||||||
// Delete associated clients and the OLM in a transaction
|
// Delete associated clients and the OLM in a transaction
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Find all clients associated with this OLM
|
// Find all clients associated with this OLM
|
||||||
@@ -57,7 +58,6 @@ export async function deleteUserOlm(
|
|||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.olmId, olmId));
|
.where(eq(clients.olmId, olmId));
|
||||||
|
|
||||||
let deletedClient: Client | null = null;
|
|
||||||
// Delete all associated clients
|
// Delete all associated clients
|
||||||
if (associatedClients.length > 0) {
|
if (associatedClients.length > 0) {
|
||||||
[deletedClient] = await trx
|
[deletedClient] = await trx
|
||||||
@@ -67,22 +67,27 @@ export async function deleteUserOlm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finally, delete the OLM itself
|
// Finally, delete the OLM itself
|
||||||
const [olm] = await trx
|
await trx.delete(olms).where(eq(olms.olmId, olmId)).returning();
|
||||||
.delete(olms)
|
});
|
||||||
.where(eq(olms.olmId, olmId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (deletedClient) {
|
if (deletedClient) {
|
||||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||||
if (olm) {
|
(e) => {
|
||||||
await sendTerminateClient(
|
logger.error(
|
||||||
|
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sendTerminateClient(
|
||||||
deletedClient.clientId,
|
deletedClient.clientId,
|
||||||
OlmErrorCodes.TERMINATED_DELETED,
|
OlmErrorCodes.TERMINATED_DELETED,
|
||||||
olm.olmId
|
olmId
|
||||||
); // the olmId needs to be provided because it cant look it up after deletion
|
).catch((e) => {
|
||||||
}
|
logger.error(
|
||||||
}
|
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting OLM ${olmId}: ${e}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ import { canCompress } from "@server/lib/clientVersionChecks";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
logger.info("Handling register olm message!");
|
logger.info("[handleOlmRegisterMessage] Handling register olm message");
|
||||||
const { message, client: c, sendToClient } = context;
|
const { message, client: c, sendToClient } = context;
|
||||||
const olm = c as Olm;
|
const olm = c as Olm;
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
if (!olm) {
|
if (!olm) {
|
||||||
logger.warn("Olm not found");
|
logger.warn("[handleOlmRegisterMessage] Olm not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +46,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
} = message.data;
|
} = message.data;
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
logger.warn("Olm client ID not found");
|
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Handling fingerprint insertion for olm register...", {
|
logger.debug(
|
||||||
|
"[handleOlmRegisterMessage] Handling fingerprint insertion for olm register...",
|
||||||
|
{
|
||||||
olmId: olm.olmId,
|
olmId: olm.olmId,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
postures
|
postures
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
||||||
|
|
||||||
@@ -85,14 +88,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
logger.warn("Client ID not found");
|
logger.warn("[handleOlmRegisterMessage] Client not found", {
|
||||||
|
clientId: olm.clientId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.blocked) {
|
if (client.blocked) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Client ${client.clientId} is blocked. Ignoring register.`
|
`[handleOlmRegisterMessage] Client ${client.clientId} is blocked. Ignoring register.`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -100,7 +106,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
if (client.approvalState == "pending") {
|
if (client.approvalState == "pending") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Client ${client.clientId} approval is pending. Ignoring register.`
|
`[handleOlmRegisterMessage] Client ${client.clientId} approval is pending. Ignoring register.`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -128,14 +135,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
logger.warn("Org not found");
|
logger.warn("[handleOlmRegisterMessage] Org not found", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
if (!olm.userId) {
|
if (!olm.userId) {
|
||||||
logger.warn("Olm has no user ID");
|
logger.warn("[handleOlmRegisterMessage] Olm has no user ID", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,12 +154,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
const { session: userSession, user } =
|
const { session: userSession, user } =
|
||||||
await validateSessionToken(userToken);
|
await validateSessionToken(userToken);
|
||||||
if (!userSession || !user) {
|
if (!userSession || !user) {
|
||||||
logger.warn("Invalid user session for olm register");
|
logger.warn(
|
||||||
|
"[handleOlmRegisterMessage] Invalid user session for olm register",
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId);
|
sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (user.userId !== olm.userId) {
|
if (user.userId !== olm.userId) {
|
||||||
logger.warn("User ID mismatch for olm register");
|
logger.warn(
|
||||||
|
"[handleOlmRegisterMessage] User ID mismatch for olm register",
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId);
|
sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -163,11 +180,15 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
sessionId // this is the user token passed in the message
|
sessionId // this is the user token passed in the message
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Policy check result:", policyCheck);
|
logger.debug("[handleOlmRegisterMessage] Policy check result", {
|
||||||
|
orgId: client.orgId,
|
||||||
|
policyCheck
|
||||||
|
});
|
||||||
|
|
||||||
if (policyCheck?.error) {
|
if (policyCheck?.error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`
|
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -175,7 +196,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED,
|
||||||
@@ -186,7 +208,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
policyCheck.policies?.maxSessionLength?.compliant === false
|
policyCheck.policies?.maxSessionLength?.compliant === false
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant session length for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED,
|
||||||
@@ -195,7 +218,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
} else if (policyCheck.policies?.requiredTwoFactor === false) {
|
} else if (policyCheck.policies?.requiredTwoFactor === false) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED,
|
||||||
@@ -204,7 +228,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
} else if (!policyCheck.allowed) {
|
} else if (!policyCheck.allowed) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -226,29 +251,39 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||||
|
|
||||||
// Prepare an array to store site configurations
|
// Prepare an array to store site configurations
|
||||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
logger.debug(
|
||||||
|
`[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
|
|
||||||
let jitMode = false;
|
let jitMode = false;
|
||||||
if (sitesCount > 250 && build == "saas") {
|
if (sitesCount > 250 && build == "saas") {
|
||||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
// THIS IS THE MAX ON THE BUSINESS TIER
|
||||||
// we have too many sites
|
// we have too many sites
|
||||||
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
||||||
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount);
|
logger.info(
|
||||||
|
`[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
jitMode = true;
|
jitMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
`[handleOlmRegisterMessage] Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
logger.warn("Public key not provided");
|
logger.warn("[handleOlmRegisterMessage] Public key not provided", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.pubKey !== publicKey || client.archived) {
|
if (client.pubKey !== publicKey || client.archived) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"Public key mismatch. Updating public key and clearing session info..."
|
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
// Update the client's public key
|
// Update the client's public key
|
||||||
await db
|
await db
|
||||||
@@ -274,7 +309,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export * from "./getResourcePolicy";
|
|
||||||
export * from "./updateResourcePolicy";
|
|
||||||
export * from "./setResourcePolicyAccessControl";
|
|
||||||
export * from "./setResourcePolicyPassword";
|
|
||||||
export * from "./setResourcePolicyPincode";
|
|
||||||
export * from "./setResourcePolicyHeaderAuth";
|
|
||||||
export * from "./setResourcePolicyWhitelist";
|
|
||||||
export * from "./setResourcePolicyRules";
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
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,19 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { build } from "@server/build";
|
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||||
import {
|
import {
|
||||||
db,
|
domains,
|
||||||
loginPage,
|
orgDomains,
|
||||||
orgs,
|
orgs,
|
||||||
Resource,
|
Resource,
|
||||||
resources,
|
resources,
|
||||||
resourcePolicies,
|
|
||||||
roleResources,
|
roleResources,
|
||||||
rolePolicies,
|
|
||||||
roles,
|
roles,
|
||||||
userPolicies,
|
userResources
|
||||||
userResources,
|
|
||||||
domainNamespaces
|
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -24,18 +20,13 @@ import logger from "@server/logger";
|
|||||||
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { build } from "@server/build";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import {
|
import { getUniqueResourceName } from "@server/db/names";
|
||||||
validateAndConstructDomain,
|
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||||
checkWildcardDomainConflict
|
|
||||||
} from "@server/lib/domainUtils";
|
|
||||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import {
|
|
||||||
getUniqueResourceName,
|
|
||||||
getUniqueResourcePolicyName
|
|
||||||
} from "@server/db/names";
|
|
||||||
|
|
||||||
const createResourceParamsSchema = z.strictObject({
|
const createResourceParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -320,46 +311,8 @@ async function createHttpResource(
|
|||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
const niceId = await getUniqueResourceName(orgId);
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
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
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
@@ -375,11 +328,22 @@ async function createHttpResource(
|
|||||||
stickySession: stickySession,
|
stickySession: stickySession,
|
||||||
postAuthPath: postAuthPath,
|
postAuthPath: postAuthPath,
|
||||||
wildcard,
|
wildcard,
|
||||||
health: "unknown",
|
health: "unknown"
|
||||||
defaultResourcePolicyId: defaultPolicy.resourcePolicyId
|
|
||||||
})
|
})
|
||||||
.returning();
|
.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({
|
await trx.insert(roleResources).values({
|
||||||
roleId: adminRole[0].roleId,
|
roleId: adminRole[0].roleId,
|
||||||
resourceId: newResource[0].resourceId
|
resourceId: newResource[0].resourceId
|
||||||
@@ -405,7 +369,7 @@ async function createHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (build !== "oss") {
|
if (build != "oss") {
|
||||||
await createCertificate(domainId, fullDomain, db);
|
await createCertificate(domainId, fullDomain, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,10 +410,22 @@ async function createRawResource(
|
|||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
const niceId = await getUniqueResourceName(orgId);
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const adminRole = await trx
|
const newResource = await trx
|
||||||
|
.insert(resources)
|
||||||
|
.values({
|
||||||
|
niceId,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
http,
|
||||||
|
protocol,
|
||||||
|
proxyPort
|
||||||
|
// enableProxy
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const adminRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
@@ -461,44 +437,6 @@ 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({
|
await trx.insert(roleResources).values({
|
||||||
roleId: adminRole[0].roleId,
|
roleId: adminRole[0].roleId,
|
||||||
resourceId: newResource[0].resourceId
|
resourceId: newResource[0].resourceId
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import { eq, inArray } from "drizzle-orm";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import {
|
|
||||||
db,
|
|
||||||
newts,
|
|
||||||
resourcePolicies,
|
|
||||||
resources,
|
|
||||||
sites,
|
|
||||||
targetHealthCheck,
|
|
||||||
targets
|
|
||||||
} 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 { NextFunction, Request, Response } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import { z } from "zod";
|
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 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 { fromError } from "zod-validation-error";
|
||||||
|
import { addPeer } from "../gerbil/peers";
|
||||||
import { removeTargets } from "../newt/targets";
|
import { removeTargets } from "../newt/targets";
|
||||||
|
import { getAllowedIps } from "../target/helpers";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
// Define Zod schema for request parameters validation
|
// Define Zod schema for request parameters validation
|
||||||
const deleteResourceSchema = z.strictObject({
|
const deleteResourceSchema = z.strictObject({
|
||||||
@@ -118,18 +113,6 @@ 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, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resourcePolicies,
|
resourceHeaderAuth,
|
||||||
resourcePolicyHeaderAuth,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePolicyPassword,
|
resourcePassword,
|
||||||
resourcePolicyPincode,
|
resourcePincode,
|
||||||
resources
|
resources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -60,53 +60,64 @@ export async function getResourceAuthInfo(
|
|||||||
|
|
||||||
const isGuidInteger = /^\d+$/.test(resourceGuid);
|
const isGuidInteger = /^\d+$/.test(resourceGuid);
|
||||||
|
|
||||||
const buildQuery = (whereClause: ReturnType<typeof eq>) =>
|
const [result] =
|
||||||
db
|
isGuidInteger && build === "saas"
|
||||||
|
? await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePolicies,
|
resourcePincode,
|
||||||
or(
|
eq(resourcePincode.resourceId, resources.resourceId)
|
||||||
eq(
|
|
||||||
resourcePolicies.resourcePolicyId,
|
|
||||||
resources.resourcePolicyId
|
|
||||||
),
|
|
||||||
eq(
|
|
||||||
resourcePolicies.resourcePolicyId,
|
|
||||||
resources.defaultResourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePolicyPincode,
|
resourcePassword,
|
||||||
eq(
|
eq(resourcePassword.resourceId, resources.resourceId)
|
||||||
resourcePolicyPincode.resourcePolicyId,
|
|
||||||
resourcePolicies.resourcePolicyId
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePolicyPassword,
|
|
||||||
eq(
|
|
||||||
resourcePolicyPassword.resourcePolicyId,
|
|
||||||
resourcePolicies.resourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePolicyHeaderAuth,
|
|
||||||
eq(
|
|
||||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
|
||||||
resourcePolicies.resourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(whereClause)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const [result] =
|
.leftJoin(
|
||||||
isGuidInteger && build === "saas"
|
resourceHeaderAuth,
|
||||||
? await buildQuery(
|
eq(
|
||||||
eq(resources.resourceId, Number(resourceGuid))
|
resourceHeaderAuth.resourceId,
|
||||||
|
resources.resourceId
|
||||||
)
|
)
|
||||||
: await buildQuery(eq(resources.resourceGuid, resourceGuid));
|
)
|
||||||
|
.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);
|
||||||
|
|
||||||
const resource = result?.resources;
|
const resource = result?.resources;
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
@@ -115,10 +126,11 @@ export async function getResourceAuthInfo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const policy = result?.resourcePolicies;
|
const pincode = result?.resourcePincode;
|
||||||
const pincode = result?.resourcePolicyPincode;
|
const password = result?.resourcePassword;
|
||||||
const password = result?.resourcePolicyPassword;
|
const headerAuth = result?.resourceHeaderAuth;
|
||||||
const headerAuth = result?.resourcePolicyHeaderAuth;
|
const headerAuthExtendedCompatibility =
|
||||||
|
result?.resourceHeaderAuthExtendedCompatibility;
|
||||||
|
|
||||||
const url = resource.fullDomain
|
const url = resource.fullDomain
|
||||||
? `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
? `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
||||||
@@ -134,13 +146,13 @@ export async function getResourceAuthInfo(
|
|||||||
pincode: pincode !== null,
|
pincode: pincode !== null,
|
||||||
headerAuth: headerAuth !== null,
|
headerAuth: headerAuth !== null,
|
||||||
headerAuthExtendedCompatibility:
|
headerAuthExtendedCompatibility:
|
||||||
headerAuth?.extendedCompatibility ?? false,
|
headerAuthExtendedCompatibility !== null,
|
||||||
sso: policy?.sso ?? false,
|
sso: resource.sso,
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
url: url ?? "",
|
url: url ?? "",
|
||||||
wildcard: resource.wildcard ?? false,
|
wildcard: resource.wildcard ?? false,
|
||||||
fullDomain: resource.fullDomain,
|
fullDomain: resource.fullDomain,
|
||||||
whitelist: policy?.emailWhitelistEnabled ?? false,
|
whitelist: resource.emailWhitelistEnabled,
|
||||||
skipToIdpId: resource.skipToIdpId,
|
skipToIdpId: resource.skipToIdpId,
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
postAuthPath: resource.postAuthPath ?? null
|
postAuthPath: resource.postAuthPath ?? null
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
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,6 +151,8 @@ export async function getUserResources(
|
|||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
scheme: string | null;
|
scheme: string | null;
|
||||||
|
ssl: boolean;
|
||||||
|
fullDomain: string | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
@@ -164,6 +166,8 @@ export async function getUserResources(
|
|||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
scheme: siteResources.scheme,
|
scheme: siteResources.scheme,
|
||||||
|
ssl: siteResources.ssl,
|
||||||
|
fullDomain: siteResources.fullDomain,
|
||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
alias: siteResources.alias,
|
alias: siteResources.alias,
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
@@ -251,6 +255,8 @@ export async function getUserResources(
|
|||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
mode: siteResource.mode,
|
mode: siteResource.mode,
|
||||||
protocol: siteResource.scheme,
|
protocol: siteResource.scheme,
|
||||||
|
ssl: siteResource.ssl,
|
||||||
|
fullDomain: siteResource.fullDomain,
|
||||||
enabled: siteResource.enabled,
|
enabled: siteResource.enabled,
|
||||||
alias: siteResource.alias,
|
alias: siteResource.alias,
|
||||||
aliasAddress: siteResource.aliasAddress,
|
aliasAddress: siteResource.aliasAddress,
|
||||||
@@ -296,6 +302,8 @@ export type GetUserResourcesResponse = {
|
|||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
protocol: string | null;
|
protocol: string | null;
|
||||||
|
ssl: boolean;
|
||||||
|
fullDomain: string | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
|
|||||||
@@ -33,4 +33,3 @@ export * from "./removeUserFromResource";
|
|||||||
export * from "./listAllResourceNames";
|
export * from "./listAllResourceNames";
|
||||||
export * from "./removeEmailFromResourceWhitelist";
|
export * from "./removeEmailFromResourceWhitelist";
|
||||||
export * from "./getStatusHistory";
|
export * from "./getStatusHistory";
|
||||||
export * from "./getResourcePolicies";
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resourcePolicies,
|
resourceHeaderAuth,
|
||||||
resourcePolicyHeaderAuth,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePolicyPassword,
|
resourcePassword,
|
||||||
resourcePolicyPincode,
|
resourcePincode,
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
sites,
|
sites,
|
||||||
@@ -163,10 +163,10 @@ function queryResourcesBase() {
|
|||||||
name: resources.name,
|
name: resources.name,
|
||||||
ssl: resources.ssl,
|
ssl: resources.ssl,
|
||||||
fullDomain: resources.fullDomain,
|
fullDomain: resources.fullDomain,
|
||||||
passwordId: resourcePolicyPassword.passwordId,
|
passwordId: resourcePassword.passwordId,
|
||||||
sso: resourcePolicies.sso,
|
sso: resources.sso,
|
||||||
pincodeId: resourcePolicyPincode.pincodeId,
|
pincodeId: resourcePincode.pincodeId,
|
||||||
whitelist: resourcePolicies.emailWhitelistEnabled,
|
whitelist: resources.emailWhitelistEnabled,
|
||||||
http: resources.http,
|
http: resources.http,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
@@ -174,45 +174,29 @@ function queryResourcesBase() {
|
|||||||
domainId: resources.domainId,
|
domainId: resources.domainId,
|
||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
wildcard: resources.wildcard,
|
wildcard: resources.wildcard,
|
||||||
health: resources.health,
|
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||||
headerAuthId: resourcePolicyHeaderAuth.headerAuthId,
|
headerAuthExtendedCompatibilityId:
|
||||||
headerAuthExtendedCompatibility:
|
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
||||||
resourcePolicyHeaderAuth.extendedCompatibility
|
health: resources.health
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePolicies,
|
resourcePassword,
|
||||||
or(
|
eq(resourcePassword.resourceId, resources.resourceId)
|
||||||
eq(
|
|
||||||
resourcePolicies.resourcePolicyId,
|
|
||||||
resources.resourcePolicyId
|
|
||||||
),
|
|
||||||
eq(
|
|
||||||
resourcePolicies.resourcePolicyId,
|
|
||||||
resources.defaultResourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
.leftJoin(
|
|
||||||
resourcePolicyPassword,
|
|
||||||
eq(
|
|
||||||
resourcePolicyPassword.resourcePolicyId,
|
|
||||||
resourcePolicies.resourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePolicyPincode,
|
resourcePincode,
|
||||||
eq(
|
eq(resourcePincode.resourceId, resources.resourceId)
|
||||||
resourcePolicyPincode.resourcePolicyId,
|
|
||||||
resourcePolicies.resourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePolicyHeaderAuth,
|
resourceHeaderAuth,
|
||||||
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(
|
eq(
|
||||||
resourcePolicyHeaderAuth.resourcePolicyId,
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
resourcePolicies.resourcePolicyId
|
resources.resourceId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||||
@@ -222,10 +206,10 @@ function queryResourcesBase() {
|
|||||||
)
|
)
|
||||||
.groupBy(
|
.groupBy(
|
||||||
resources.resourceId,
|
resources.resourceId,
|
||||||
resourcePolicies.resourcePolicyId,
|
resourcePassword.passwordId,
|
||||||
resourcePolicyPassword.passwordId,
|
resourcePincode.pincodeId,
|
||||||
resourcePolicyPincode.pincodeId,
|
resourceHeaderAuth.headerAuthId,
|
||||||
resourcePolicyHeaderAuth.headerAuthId
|
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,21 +355,21 @@ export async function listResources(
|
|||||||
case "protected":
|
case "protected":
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(
|
||||||
eq(resourcePolicies.sso, true),
|
eq(resources.sso, true),
|
||||||
eq(resourcePolicies.emailWhitelistEnabled, true),
|
eq(resources.emailWhitelistEnabled, true),
|
||||||
not(isNull(resourcePolicyHeaderAuth.headerAuthId)),
|
not(isNull(resourceHeaderAuth.headerAuthId)),
|
||||||
not(isNull(resourcePolicyPincode.pincodeId)),
|
not(isNull(resourcePincode.pincodeId)),
|
||||||
not(isNull(resourcePolicyPassword.passwordId))
|
not(isNull(resourcePassword.passwordId))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "not_protected":
|
case "not_protected":
|
||||||
conditions.push(
|
conditions.push(
|
||||||
not(eq(resourcePolicies.sso, true)),
|
not(eq(resources.sso, true)),
|
||||||
not(eq(resourcePolicies.emailWhitelistEnabled, true)),
|
not(eq(resources.emailWhitelistEnabled, true)),
|
||||||
isNull(resourcePolicyHeaderAuth.headerAuthId),
|
isNull(resourceHeaderAuth.headerAuthId),
|
||||||
isNull(resourcePolicyPincode.pincodeId),
|
isNull(resourcePincode.pincodeId),
|
||||||
isNull(resourcePolicyPassword.passwordId)
|
isNull(resourcePassword.passwordId)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -462,9 +446,9 @@ export async function listResources(
|
|||||||
ssl: row.ssl,
|
ssl: row.ssl,
|
||||||
fullDomain: row.fullDomain,
|
fullDomain: row.fullDomain,
|
||||||
passwordId: row.passwordId,
|
passwordId: row.passwordId,
|
||||||
sso: row.sso ?? false,
|
sso: row.sso,
|
||||||
pincodeId: row.pincodeId,
|
pincodeId: row.pincodeId,
|
||||||
whitelist: row.whitelist ?? false,
|
whitelist: row.whitelist,
|
||||||
http: row.http,
|
http: row.http,
|
||||||
protocol: row.protocol,
|
protocol: row.protocol,
|
||||||
proxyPort: row.proxyPort,
|
proxyPort: row.proxyPort,
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import type { Resource, ResourcePolicy } from "@server/db";
|
|
||||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
|
||||||
|
|
||||||
export type GetMaintenanceInfoResponse = {
|
export type GetMaintenanceInfoResponse = {
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,19 +8,3 @@ export type GetMaintenanceInfoResponse = {
|
|||||||
maintenanceMessage: string | null;
|
maintenanceMessage: string | null;
|
||||||
maintenanceEstimatedTime: 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,23 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||||
db,
|
|
||||||
domainNamespaces,
|
|
||||||
loginPage,
|
|
||||||
resourceHeaderAuth,
|
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
|
||||||
resourcePassword,
|
|
||||||
resourcePincode,
|
|
||||||
resourceRules,
|
|
||||||
resourceWhitelist
|
|
||||||
} from "@server/db";
|
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
Org,
|
Org,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
orgs,
|
orgs,
|
||||||
Resource,
|
Resource,
|
||||||
resourcePolicies,
|
|
||||||
resources
|
resources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, ne } from "drizzle-orm";
|
import { eq, and, ne } from "drizzle-orm";
|
||||||
@@ -35,10 +24,7 @@ import {
|
|||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { OpenAPITags } from "@server/openApi";
|
import { OpenAPITags } from "@server/openApi";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import {
|
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||||
validateAndConstructDomain,
|
|
||||||
checkWildcardDomainConflict
|
|
||||||
} from "@server/lib/domainUtils";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
@@ -82,8 +68,7 @@ const updateHttpResourceBodySchema = z
|
|||||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||||
maintenanceEstimatedTime: z.string().max(100).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, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -180,8 +165,7 @@ const updateRawResourceBodySchema = z
|
|||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
proxyProtocol: 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, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -317,42 +301,6 @@ async function updateHttpResource(
|
|||||||
|
|
||||||
const updateData = parsedBody.data;
|
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) {
|
if (updateData.niceId) {
|
||||||
const [existingResource] = await db
|
const [existingResource] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -378,6 +326,10 @@ async function updateHttpResource(
|
|||||||
|
|
||||||
// Wildcard subdomains are a paid feature
|
// Wildcard subdomains are a paid feature
|
||||||
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
|
resource.orgId,
|
||||||
|
tierMatrix.wildcardSubdomain
|
||||||
|
);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -522,6 +474,10 @@ async function updateHttpResource(
|
|||||||
headers = null;
|
headers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
|
resource.orgId,
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
updateData.maintenanceModeEnabled = undefined;
|
updateData.maintenanceModeEnabled = undefined;
|
||||||
updateData.maintenanceModeType = undefined;
|
updateData.maintenanceModeType = undefined;
|
||||||
@@ -579,92 +535,9 @@ async function updateRawResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
let updatedResource: Resource | null = null;
|
|
||||||
|
|
||||||
const [existingResource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resource.resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
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) {
|
if (updateData.niceId) {
|
||||||
const [existingResourceConflict] = await trx
|
const [existingResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(
|
.where(
|
||||||
@@ -675,8 +548,8 @@ async function updateRawResource(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existingResourceConflict &&
|
existingResource &&
|
||||||
existingResourceConflict.resourceId !== resource.resourceId
|
existingResource.resourceId !== resource.resourceId
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -687,14 +560,13 @@ async function updateRawResource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[updatedResource] = await trx
|
const updatedResource = await db
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
.where(eq(resources.resourceId, resource.resourceId))
|
.where(eq(resources.resourceId, resource.resourceId))
|
||||||
.returning();
|
.returning();
|
||||||
});
|
|
||||||
|
|
||||||
if (!updatedResource) {
|
if (updatedResource.length === 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
@@ -704,7 +576,7 @@ async function updateRawResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: updatedResource,
|
data: updatedResource[0],
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Non-http Resource updated successfully",
|
message: "Non-http Resource updated successfully",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const listSitesSchema = z.object({
|
|||||||
page: z.coerce
|
page: z.coerce
|
||||||
.number<string>() // for prettier formatting
|
.number<string>() // for prettier formatting
|
||||||
.int()
|
.int()
|
||||||
.positive()
|
.min(0)
|
||||||
.optional()
|
.optional()
|
||||||
.catch(1)
|
.catch(1)
|
||||||
.default(1)
|
.default(1)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
clients,
|
clients,
|
||||||
clientSiteResources,
|
clientSiteResources,
|
||||||
siteResources,
|
siteResources,
|
||||||
apiKeyOrg
|
apiKeyOrg,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -220,8 +221,12 @@ export async function batchAddClientToSiteResources(
|
|||||||
siteResourceId: siteResource.siteResourceId
|
siteResourceId: siteResource.siteResourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(client, trx);
|
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
SiteResource,
|
SiteResource,
|
||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
userSiteResources
|
userSiteResources,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { getUniqueSiteResourceName } from "@server/db/names";
|
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||||
import {
|
import {
|
||||||
@@ -74,7 +75,6 @@ const createSiteResourceSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.mode === "host") {
|
if (data.mode === "host") {
|
||||||
if (data.mode == "host") {
|
|
||||||
// Check if it's a valid IP address using zod (v4 or v6)
|
// Check if it's a valid IP address using zod (v4 or v6)
|
||||||
const isValidIP = z
|
const isValidIP = z
|
||||||
// .union([z.ipv4(), z.ipv6()])
|
// .union([z.ipv4(), z.ipv6()])
|
||||||
@@ -84,7 +84,6 @@ const createSiteResourceSchema = z
|
|||||||
if (isValidIP) {
|
if (isValidIP) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||||
const domainRegex =
|
const domainRegex =
|
||||||
@@ -96,17 +95,12 @@ const createSiteResourceSchema = z
|
|||||||
data.alias.trim() !== "";
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
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;
|
||||||
}
|
}
|
||||||
return true;
|
} else if (data.mode === "cidr") {
|
||||||
},
|
|
||||||
{
|
|
||||||
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)
|
// Check if it's a valid CIDR (v4 or v6)
|
||||||
const isValidCIDR = z
|
const isValidCIDR = z
|
||||||
.union([z.cidrv4(), z.cidrv6()])
|
.union([z.cidrv4(), z.cidrv6()])
|
||||||
@@ -116,7 +110,8 @@ const createSiteResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
message:
|
||||||
|
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -525,12 +520,10 @@ export async function createSiteResource(
|
|||||||
// own transaction so it always executes on the primary — avoiding any
|
// own transaction so it always executes on the primary — avoiding any
|
||||||
// replica-lag issues while still allowing the HTTP response to return
|
// replica-lag issues while still allowing the HTTP response to return
|
||||||
// early.
|
// early.
|
||||||
db.transaction(async (trx) => {
|
rebuildClientAssociationsFromSiteResource(
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
|
||||||
newSiteResource!,
|
newSiteResource!,
|
||||||
trx
|
primaryDb
|
||||||
);
|
).catch((err) => {
|
||||||
}).catch((err) => {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||||
err
|
err
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, newts, sites } from "@server/db";
|
import { db, newts, primaryDb, sites } from "@server/db";
|
||||||
import { siteResources } from "@server/db";
|
import { siteResources } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -73,12 +73,10 @@ export async function deleteSiteResource(
|
|||||||
// own transaction so it always executes on the primary — avoiding any
|
// own transaction so it always executes on the primary — avoiding any
|
||||||
// replica-lag issues while still allowing the HTTP response to return
|
// replica-lag issues while still allowing the HTTP response to return
|
||||||
// early.
|
// early.
|
||||||
db.transaction(async (trx) => {
|
rebuildClientAssociationsFromSiteResource(
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
|
||||||
removedSiteResource,
|
removedSiteResource,
|
||||||
trx
|
primaryDb
|
||||||
);
|
).catch((err) => {
|
||||||
}).catch((err) => {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||||
err
|
err
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ const updateSiteResourceSchema = z
|
|||||||
data.alias.trim() !== "";
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -112,21 +123,6 @@ const updateSiteResourceSchema = z
|
|||||||
"Destination must be a valid IP address or valid domain AND alias is required"
|
"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(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.mode !== "http") return true;
|
if (data.mode !== "http") return true;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, orgs } from "@server/db";
|
import { db, orgs, primaryDb } from "@server/db";
|
||||||
import { roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
|
import {
|
||||||
|
roles,
|
||||||
|
userInviteRoles,
|
||||||
|
userInvites,
|
||||||
|
userOrgs,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -146,9 +152,7 @@ export async function acceptInvite(
|
|||||||
.from(userInviteRoles)
|
.from(userInviteRoles)
|
||||||
.where(eq(userInviteRoles.inviteId, inviteId));
|
.where(eq(userInviteRoles.inviteId, inviteId));
|
||||||
|
|
||||||
const inviteRoleIds = [
|
const inviteRoleIds = [...new Set(inviteRoleRows.map((r) => r.roleId))];
|
||||||
...new Set(inviteRoleRows.map((r) => r.roleId))
|
|
||||||
];
|
|
||||||
if (inviteRoleIds.length === 0) {
|
if (inviteRoleIds.length === 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -193,13 +197,19 @@ export async function acceptInvite(
|
|||||||
.delete(userInvites)
|
.delete(userInvites)
|
||||||
.where(eq(userInvites.inviteId, inviteId));
|
.where(eq(userInvites.inviteId, inviteId));
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}`
|
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
calculateUserClientsForOrgs(existingUser[0].userId, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response<AcceptInviteResponse>(res, {
|
return response<AcceptInviteResponse>(res, {
|
||||||
data: { accepted: true, orgId: existingInvite.orgId },
|
data: { accepted: true, orgId: existingInvite.orgId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -112,6 +112,8 @@ export async function addUserRoleLegacy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -138,11 +140,19 @@ export async function addUserRoleLegacy(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { ...existingUser, roleId },
|
data: { ...existingUser, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import { users } from "@server/db";
|
import { users } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -53,8 +53,12 @@ export async function adminRemoveUser(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx.delete(users).where(eq(users.userId, userId));
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
|
});
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after removing user ${userId}: ${e}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { db, orgs } from "@server/db";
|
import { db, orgs, primaryDb } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
@@ -34,8 +34,7 @@ const bodySchema = z
|
|||||||
roleId: z.number().int().positive().optional()
|
roleId: z.number().int().positive().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(d) =>
|
(d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
||||||
(d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
|
||||||
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
||||||
)
|
)
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
@@ -100,8 +99,14 @@ export async function createOrgUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { username, email, name, type, idpId, roleIds: uniqueRoleIds } =
|
const {
|
||||||
parsedBody.data;
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
idpId,
|
||||||
|
roleIds: uniqueRoleIds
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (build == "saas") {
|
if (build == "saas") {
|
||||||
const usage = await usageService.getUsage(orgId, FeatureId.USERS);
|
const usage = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||||
@@ -232,6 +237,7 @@ export async function createOrgUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let userIdForClients: string | undefined;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const [existingUser] = await trx
|
const [existingUser] = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -270,7 +276,7 @@ export async function createOrgUser(
|
|||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
userId: existingUser.userId,
|
userId: existingUser.userId,
|
||||||
autoProvisioned: false,
|
autoProvisioned: false
|
||||||
},
|
},
|
||||||
uniqueRoleIds,
|
uniqueRoleIds,
|
||||||
trx
|
trx
|
||||||
@@ -297,15 +303,25 @@ export async function createOrgUser(
|
|||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
userId: newUser.userId,
|
userId: newUser.userId,
|
||||||
autoProvisioned: false,
|
autoProvisioned: false
|
||||||
},
|
},
|
||||||
uniqueRoleIds,
|
uniqueRoleIds,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
userIdForClients = userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userIdForClients) {
|
||||||
|
calculateUserClientsForOrgs(userIdForClients, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after creating org user: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
.from(userOrgRoles)
|
.from(userOrgRoles)
|
||||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(
|
.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);
|
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||||
@@ -143,7 +146,7 @@ export async function getOrgUser(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have permission to get organization user details"
|
"User does not have permission perform this action"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
UserOrg,
|
UserOrg,
|
||||||
userSiteResources
|
userSiteResources,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { userOrgs, userResources, users, userSites } from "@server/db";
|
import { userOrgs, userResources, users, userSites } from "@server/db";
|
||||||
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
||||||
@@ -91,25 +92,12 @@ export async function removeUserOrg(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await removeUserFromOrg(org, userId, trx);
|
await removeUserFromOrg(org, userId, trx);
|
||||||
|
});
|
||||||
|
|
||||||
// if (build === "saas") {
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
// const [rootUser] = await trx
|
logger.error(
|
||||||
// .select()
|
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}`
|
||||||
// .from(users)
|
);
|
||||||
// .where(eq(users.userId, userId));
|
|
||||||
//
|
|
||||||
// const [leftInOrgs] = await trx
|
|
||||||
// .select({ count: count() })
|
|
||||||
// .from(userOrgs)
|
|
||||||
// .where(eq(userOrgs.userId, userId));
|
|
||||||
//
|
|
||||||
// // if the user is not an internal user and does not belong to any org, delete the entire user
|
|
||||||
// if (rootUser?.type !== UserType.Internal && !leftInOrgs.count) {
|
|
||||||
// await trx.delete(users).where(eq(users.userId, userId));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default async function migration() {
|
|||||||
await db.execute(sql`BEGIN`);
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
CREATE TABLE "trialNotifications" (
|
CREATE TABLE IF NOT EXISTS "trialNotifications" (
|
||||||
"notificationId" serial PRIMARY KEY NOT NULL,
|
"notificationId" serial PRIMARY KEY NOT NULL,
|
||||||
"subscriptionId" varchar(255) NOT NULL,
|
"subscriptionId" varchar(255) NOT NULL,
|
||||||
"notificationType" varchar(50) NOT NULL,
|
"notificationType" varchar(50) NOT NULL,
|
||||||
@@ -52,10 +52,6 @@ export default async function migration() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "trialNotifications" ADD CONSTRAINT "trialNotifications_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await db.execute(sql`COMMIT`);
|
await db.execute(sql`COMMIT`);
|
||||||
console.log("Migrated database");
|
console.log("Migrated database");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -77,7 +73,7 @@ export default async function migration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
|
`Updated names for ${existingHealthChecks.length} existing targetHealthCheck row(s)`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error while migrating targetHealthCheck rows:", e);
|
console.error("Error while migrating targetHealthCheck rows:", e);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function migration() {
|
|||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
CREATE TABLE 'trialNotifications' (
|
CREATE TABLE IF NOT EXISTS 'trialNotifications' (
|
||||||
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
'subscriptionId' text NOT NULL,
|
'subscriptionId' text NOT NULL,
|
||||||
'notificationType' text NOT NULL,
|
'notificationType' text NOT NULL,
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ export default function GeneralPage() {
|
|||||||
onAutoProvisionChange={(checked) => {
|
onAutoProvisionChange={(checked) => {
|
||||||
form.setValue("autoProvision", checked);
|
form.setValue("autoProvision", checked);
|
||||||
}}
|
}}
|
||||||
|
orgId={orgId as string}
|
||||||
roleMappingMode={roleMappingMode}
|
roleMappingMode={roleMappingMode}
|
||||||
onRoleMappingModeChange={(data) => {
|
onRoleMappingModeChange={(data) => {
|
||||||
setRoleMappingMode(data);
|
setRoleMappingMode(data);
|
||||||
|
|||||||
@@ -246,7 +246,10 @@ export default function Page() {
|
|||||||
|
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
||||||
|
|
||||||
<fieldset disabled={disabled} className={disabled ? "opacity-50 pointer-events-none" : ""}>
|
<fieldset
|
||||||
|
disabled={disabled}
|
||||||
|
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
||||||
|
>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
@@ -261,7 +264,10 @@ export default function Page() {
|
|||||||
<OidcIdpProviderTypeSelect
|
<OidcIdpProviderTypeSelect
|
||||||
value={form.watch("type")}
|
value={form.watch("type")}
|
||||||
onTypeChange={(next) => {
|
onTypeChange={(next) => {
|
||||||
applyOidcIdpProviderType(form.setValue, next);
|
applyOidcIdpProviderType(
|
||||||
|
form.setValue,
|
||||||
|
next
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -318,30 +324,42 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<AutoProvisionConfigWidget
|
<AutoProvisionConfigWidget
|
||||||
autoProvision={
|
autoProvision={
|
||||||
form.watch("autoProvision") as boolean
|
form.watch(
|
||||||
|
"autoProvision"
|
||||||
|
) as boolean
|
||||||
} // is this right?
|
} // is this right?
|
||||||
onAutoProvisionChange={(checked) => {
|
onAutoProvisionChange={(checked) => {
|
||||||
form.setValue("autoProvision", checked);
|
form.setValue(
|
||||||
|
"autoProvision",
|
||||||
|
checked
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
|
orgId={params.orgId as string}
|
||||||
roleMappingMode={roleMappingMode}
|
roleMappingMode={roleMappingMode}
|
||||||
onRoleMappingModeChange={(data) => {
|
onRoleMappingModeChange={(data) => {
|
||||||
setRoleMappingMode(data);
|
setRoleMappingMode(data);
|
||||||
}}
|
}}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
fixedRoleNames={fixedRoleNames}
|
fixedRoleNames={fixedRoleNames}
|
||||||
onFixedRoleNamesChange={setFixedRoleNames}
|
onFixedRoleNamesChange={
|
||||||
|
setFixedRoleNames
|
||||||
|
}
|
||||||
mappingBuilderClaimPath={
|
mappingBuilderClaimPath={
|
||||||
mappingBuilderClaimPath
|
mappingBuilderClaimPath
|
||||||
}
|
}
|
||||||
onMappingBuilderClaimPathChange={
|
onMappingBuilderClaimPathChange={
|
||||||
setMappingBuilderClaimPath
|
setMappingBuilderClaimPath
|
||||||
}
|
}
|
||||||
mappingBuilderRules={mappingBuilderRules}
|
mappingBuilderRules={
|
||||||
|
mappingBuilderRules
|
||||||
|
}
|
||||||
onMappingBuilderRulesChange={
|
onMappingBuilderRulesChange={
|
||||||
setMappingBuilderRules
|
setMappingBuilderRules
|
||||||
}
|
}
|
||||||
rawExpression={rawRoleExpression}
|
rawExpression={rawRoleExpression}
|
||||||
onRawExpressionChange={setRawRoleExpression}
|
onRawExpressionChange={
|
||||||
|
setRawRoleExpression
|
||||||
|
}
|
||||||
orgMappingField={{
|
orgMappingField={{
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "orgMapping"
|
name: "orgMapping"
|
||||||
@@ -368,7 +386,9 @@ export default function Page() {
|
|||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-idp-form"
|
id="create-idp-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -397,7 +417,9 @@ export default function Page() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpClientSecret")}
|
{t(
|
||||||
|
"idpClientSecret"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -437,7 +459,9 @@ export default function Page() {
|
|||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-idp-form"
|
id="create-idp-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -445,7 +469,9 @@ export default function Page() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpTenantIdLabel")}
|
{t(
|
||||||
|
"idpTenantIdLabel"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
@@ -487,7 +513,9 @@ export default function Page() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpClientSecret")}
|
{t(
|
||||||
|
"idpClientSecret"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -527,7 +555,9 @@ export default function Page() {
|
|||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-idp-form"
|
id="create-idp-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -556,7 +586,9 @@ export default function Page() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpClientSecret")}
|
{t(
|
||||||
|
"idpClientSecret"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -640,7 +672,9 @@ export default function Page() {
|
|||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-idp-form"
|
id="create-idp-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -648,7 +682,9 @@ export default function Page() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpJmespathLabel")}
|
{t(
|
||||||
|
"idpJmespathLabel"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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,10 +3,12 @@
|
|||||||
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default function NewAlertRulePage() {
|
export default function NewAlertRulePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -14,6 +16,19 @@ export default function NewAlertRulePage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Layout } from "@app/components/Layout";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { orgNavSections } from "@app/app/navigation";
|
import { orgNavSections } from "@app/app/navigation";
|
||||||
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -49,7 +48,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const orgUser = await getCachedOrgUser(params.orgId, user.userId);
|
const getOrgUser = cache(() =>
|
||||||
|
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||||
|
`/org/${params.orgId}/user/${user.userId}`,
|
||||||
|
cookie
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const orgUser = await getOrgUser();
|
||||||
|
|
||||||
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
|
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
|
||||||
throw new Error(t("userErrorNotAdminOrOwner"));
|
throw new Error(t("userErrorNotAdminOrOwner"));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user