mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-15 04:39:54 +00:00
Compare commits
186 Commits
1.18.2-s.3
...
redis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db02f482ff | ||
|
|
efb1d69ac9 | ||
|
|
107986d848 | ||
|
|
b6c8fbe43b | ||
|
|
4208a9f372 | ||
|
|
3c82a228fb | ||
|
|
a4aa29e48a | ||
|
|
0f82ba6627 | ||
|
|
1df5d9fac8 | ||
|
|
5189583d73 | ||
|
|
b794d2aa40 | ||
|
|
c69059b227 | ||
|
|
b27b62d4c8 | ||
|
|
ee8290d68c | ||
|
|
82e8e79b16 | ||
|
|
2d428d2fa0 | ||
|
|
0005c11a0a | ||
|
|
f91d914ec6 | ||
|
|
b6caeda0a5 | ||
|
|
77d17af15b | ||
|
|
264c6bf4e8 | ||
|
|
4aa72eb1a3 | ||
|
|
a066a68e1a | ||
|
|
9fb677e952 | ||
|
|
88d8414eb8 | ||
|
|
5f3fafb1b0 | ||
|
|
de1338a8cd | ||
|
|
0800aa2a61 | ||
|
|
4959d66ac1 | ||
|
|
9320df8be6 | ||
|
|
13ec6b6620 | ||
|
|
2ca3ef019c | ||
|
|
724e41a54f | ||
|
|
ce5e62d216 | ||
|
|
874dc2b33e | ||
|
|
3b2622d590 | ||
|
|
c81d855741 | ||
|
|
3bce8d3596 | ||
|
|
ee2a1e2bc3 | ||
|
|
a0f3ee74f9 | ||
|
|
82a36fd632 | ||
|
|
c5084137ab | ||
|
|
65ec8da100 | ||
|
|
e76e7581a5 | ||
|
|
a97a4b6ec1 | ||
|
|
e38bbde348 | ||
|
|
026260ddfb | ||
|
|
97be5eb7d5 | ||
|
|
d7b96ba3f5 | ||
|
|
b42672530f | ||
|
|
b6b2dbd8ab | ||
|
|
975f3a01f5 | ||
|
|
4de2dfff85 | ||
|
|
27d230647f | ||
|
|
114486608e | ||
|
|
10fa9274d0 | ||
|
|
cbdc74768f | ||
|
|
10f95896aa | ||
|
|
5b8994d143 | ||
|
|
c46ef2fe9c | ||
|
|
4cd025dd91 | ||
|
|
ce04ea9720 | ||
|
|
a3ce382725 | ||
|
|
4eb49e3e60 | ||
|
|
2a9481023a | ||
|
|
8ed01372b8 | ||
|
|
6a7d4fd385 | ||
|
|
7bc08c0425 | ||
|
|
451f3d24a8 | ||
|
|
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 | ||
|
|
7436aebca7 | ||
|
|
66fda553e4 | ||
|
|
432dc81875 | ||
|
|
2ecf076c0f | ||
|
|
9b71c426c7 | ||
|
|
e06dda27cb | ||
|
|
18f6e0f75d | ||
|
|
3b232bcc58 | ||
|
|
c575bb76e7 | ||
|
|
1ca1059673 | ||
|
|
49d22498fc | ||
|
|
30e627cca8 | ||
|
|
53c1e2e742 | ||
|
|
657072dd17 | ||
|
|
443a19165f | ||
|
|
b4906ec9ba | ||
|
|
39bf64bc35 | ||
|
|
a3f30eff02 | ||
|
|
081940dff8 | ||
|
|
c4cf4cdec4 | ||
|
|
85f2165a1e | ||
|
|
1bc7175dd4 | ||
|
|
ddaa9c32a7 | ||
|
|
27b2ec309d | ||
|
|
91ce8bea4b | ||
|
|
2ea9d27237 | ||
|
|
95cbaaae21 | ||
|
|
955aa41f53 | ||
|
|
cb3fa028c3 | ||
|
|
c746e1bc8d | ||
|
|
da4dd88fdd | ||
|
|
b9bee2836b | ||
|
|
53c48e6f04 | ||
|
|
9db5ff9ff7 |
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
60
cli/commands/disableUser2fa.ts
Normal file
60
cli/commands/disableUser2fa.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for a user by email address.
|
||||||
|
*/
|
||||||
|
type DisableUser2faArgs = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disableUser2fa: CommandModule<{}, DisableUser2faArgs> = {
|
||||||
|
command: "disable-user-2fa",
|
||||||
|
describe: "Disable 2FA for a user (sets twoFactorEnabled=false, clears secret)",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs.option("email", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "User email address"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { email: string }) => {
|
||||||
|
try {
|
||||||
|
const { email } = argv;
|
||||||
|
console.log(`Looking for user with email: ${email}`);
|
||||||
|
|
||||||
|
// Find the user by email
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error(`User with email '${email}' not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.twoFactorEnabled) {
|
||||||
|
console.log(`2FA is already disabled for user '${email}'.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user: disable 2FA and clear secret
|
||||||
|
await db.update(users)
|
||||||
|
.set({
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
twoFactorSecret: null,
|
||||||
|
twoFactorSetupRequested: false
|
||||||
|
})
|
||||||
|
.where(eq(users.userId, user.userId));
|
||||||
|
|
||||||
|
console.log(`2FA disabled for user '${email}'.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error disabling 2FA:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,8 @@ 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";
|
||||||
|
import { disableUser2fa } from "./commands/disableUser2fa";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -19,5 +21,7 @@ yargs(hideBin(process.argv))
|
|||||||
.command(clearLicenseKeys)
|
.command(clearLicenseKeys)
|
||||||
.command(deleteClient)
|
.command(deleteClient)
|
||||||
.command(generateOrgCaKeys)
|
.command(generateOrgCaKeys)
|
||||||
|
.command(clearCertificates)
|
||||||
|
.command(disableUser2fa)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
12
docker-compose.mailpit.yml
Normal file
12
docker-compose.mailpit.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
mailer:
|
||||||
|
image: axllent/mailpit
|
||||||
|
ports:
|
||||||
|
- 8025:8025
|
||||||
|
- 1025:1025
|
||||||
|
volumes:
|
||||||
|
- mailpit-storage:/data
|
||||||
|
environment:
|
||||||
|
- MP_DATABASE=/data/mailpit.db
|
||||||
|
volumes:
|
||||||
|
mailpit-storage:
|
||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката",
|
"shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката",
|
||||||
"shareDeleted": "Връзката беше изтрита",
|
"shareDeleted": "Връзката беше изтрита",
|
||||||
"shareDeletedDescription": "Връзката беше премахната",
|
"shareDeletedDescription": "Връзката беше премахната",
|
||||||
|
"shareDelete": "Изтрийте споделената връзка",
|
||||||
|
"shareDeleteConfirm": "Потвърдете изтриването на споделената връзка",
|
||||||
|
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
|
||||||
|
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
|
||||||
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
|
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
|
||||||
"accessToken": "Достъп Токен",
|
"accessToken": "Достъп Токен",
|
||||||
"usageExamples": "Примери за използване",
|
"usageExamples": "Примери за използване",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
|
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
|
||||||
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
|
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
|
||||||
"userRemoveOrg": "Премахване на потребителя от организацията",
|
"userRemoveOrg": "Премахване на потребителя от организацията",
|
||||||
|
"userQuestionOrgRemoveSelf": "Сигурни ли сте, че искате да премахнете себе си от тази организация?",
|
||||||
|
"userMessageOrgRemoveSelf": "Ще загубите достъп незабавно. Администратор може да ви покани отново по-късно, но ще трябва да приемете нова покана.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Потвърдете премахването на себе си",
|
||||||
|
"userRemoveOrgSelf": "Премахнете себе си от организацията",
|
||||||
|
"userRemoveOrgSelfWarning": "Ще загубите достъп до тази организация незабавно.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ПРЕМАХНЕТЕ МЕ ОТ ОРГАНИЗАЦИЯТА",
|
||||||
"users": "Потребители",
|
"users": "Потребители",
|
||||||
"accessRoleMember": "Член",
|
"accessRoleMember": "Член",
|
||||||
"accessRoleOwner": "Собственик",
|
"accessRoleOwner": "Собственик",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Невалиден имейл адрес",
|
"emailInvalid": "Невалиден имейл адрес",
|
||||||
"inviteValidityDuration": "Моля, изберете продължителност",
|
"inviteValidityDuration": "Моля, изберете продължителност",
|
||||||
"accessRoleSelectPlease": "Моля, изберете роля",
|
"accessRoleSelectPlease": "Моля, изберете роля",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Премахване на административния ви достъп?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "След записване няма да имате повече администраторски права в тази организация. Друг администратор може да възстанови достъпа, ако е необходимо.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Премахнете административния ми достъп",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ПРЕМАХНЕТЕ АДМИНИСТРАТИВНИЯ МИ ДОСТЪП",
|
||||||
|
"ownerMustRetainAdminRole": "Собственикът на организацията трябва да запази поне една администраторска роля.",
|
||||||
"usernameRequired": "Необходимо е потребителско име",
|
"usernameRequired": "Необходимо е потребителско име",
|
||||||
"idpSelectPlease": "Моля, изберете доставчик на идентичност",
|
"idpSelectPlease": "Моля, изберете доставчик на идентичност",
|
||||||
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
|
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
||||||
"targetsSubmit": "Запазване на целите",
|
"targetsSubmit": "Запазване на целите",
|
||||||
"addTarget": "Добавете цел",
|
"addTarget": "Добавете цел",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Роунд Робин маршрутизирането няма да работи между сайтове, които не са свързани към един и същ възел, но автоматичното превключване ще работи.",
|
||||||
"targetErrorInvalidIp": "Невалиден IP адрес",
|
"targetErrorInvalidIp": "Невалиден IP адрес",
|
||||||
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
||||||
"targetErrorInvalidPort": "Невалиден порт",
|
"targetErrorInvalidPort": "Невалиден порт",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Валидна парола",
|
"validPassword": "Валидна парола",
|
||||||
"validEmail": "Валиден имейл",
|
"validEmail": "Валиден имейл",
|
||||||
"validSSO": "Валидно SSO",
|
"validSSO": "Валидно SSO",
|
||||||
|
"view": "Преглед",
|
||||||
|
"configManaged": "Управлявана конфигурация",
|
||||||
"connectedClient": "Свързан клиент",
|
"connectedClient": "Свързан клиент",
|
||||||
"resourceBlocked": "Блокирани ресурси",
|
"resourceBlocked": "Блокирани ресурси",
|
||||||
"droppedByRule": "Прекратено от правило",
|
"droppedByRule": "Прекратено от правило",
|
||||||
@@ -2660,19 +2678,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": "Колко дълго да се задържат логовете за достъп",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
|
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
|
||||||
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
|
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
|
||||||
"streamingFailedToLoad": "Неуспешно зареждане на дестинации",
|
"streamingLastSyncError": "Възникна грешка при последната синхронизация",
|
||||||
"streamingUnexpectedError": "Възникна неочаквана грешка.",
|
"streamingUnexpectedError": "Възникна неочаквана грешка.",
|
||||||
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
|
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
|
||||||
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
|
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Редактиране на дестинацията",
|
"S3DestEditTitle": "Редактиране на дестинацията",
|
||||||
"S3DestAddTitle": "Добавете S3 дестинация",
|
"S3DestAddTitle": "Добавете S3 дестинация",
|
||||||
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
|
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
|
||||||
"S3DestAddDescription": "Конфигурирайте нов крайна точка на S3, за да получавате събития на вашата организация.",
|
"S3DestAddDescription": "Конфигурирайте ново хранилище Amazon S3 (или съвместимо с S3), за да получавате събития на вашата организация.",
|
||||||
|
"s3DestTabSettings": "Настройки",
|
||||||
|
"s3DestTabFormat": "Формат",
|
||||||
|
"s3DestNameLabel": "Име",
|
||||||
|
"s3DestNamePlaceholder": "Моята S3 дестинация",
|
||||||
|
"s3DestAccessKeyIdLabel": "Идентификатор на достъп за AWS Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Тайният ключ за достъп на AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Вашият таен ключ за достъп за AWS",
|
||||||
|
"s3DestRegionLabel": "AWS Регион",
|
||||||
|
"s3DestBucketLabel": "Име на хранилище",
|
||||||
|
"s3DestPrefixLabel": "Префикс на ключ (по избор)",
|
||||||
|
"s3DestPrefixDescription": "По избор пътеводен префикс, добавен към всеки обектен ключ. Обектите се съхраняват в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Потребителски крайна точка (по избор)",
|
||||||
|
"s3DestEndpointDescription": "Заместете крайната точка на S3 за съвместимо с S3 хранилище като MinIO или Cloudflare R2. Оставете празно за стандартното AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip компресия",
|
||||||
|
"s3DestGzipDescription": "Компресирайте всеки качен обект с gzip. Намалява разходите за съхранение и размера на качването.",
|
||||||
|
"s3DestFormatTitle": "Формат на файл",
|
||||||
|
"s3DestFormatDescription": "Как събитията са сериализирани вътре във всеки качен обект.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Всеки обект е JSON масив от записи на събития. Съвместим с повечето аналитични инструменти.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Всеки обект съдържа един JSON запис на ред (форматиран JSON с нов ред). Съвместим с Athena, BigQuery и Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Всеки обект е RFC-4180 CSV файл с ред заглавие. Имената на колоните са извлечени от полетата на данните за събитията.",
|
||||||
|
"s3DestSaveChanges": "Запази промените",
|
||||||
|
"s3DestCreateDestination": "Създаване на дестинация",
|
||||||
|
"s3DestUpdatedSuccess": "Дестинацията е актуализирана успешно",
|
||||||
|
"s3DestCreatedSuccess": "Дестинацията е създадена успешно",
|
||||||
|
"s3DestUpdateFailed": "Неуспешно актуализиране на дестинацията",
|
||||||
|
"s3DestCreateFailed": "Неуспешно създаване на дестинация",
|
||||||
"datadogDestEditTitle": "Редактиране на дестинация",
|
"datadogDestEditTitle": "Редактиране на дестинация",
|
||||||
"datadogDestAddTitle": "Добавяне на Datadog дестинация",
|
"datadogDestAddTitle": "Добавяне на Datadog дестинация",
|
||||||
"datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.",
|
"datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.",
|
||||||
@@ -3134,7 +3179,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
||||||
"httpDestConnectionLogsTitle": "Логове на връзката",
|
"httpDestConnectionLogsTitle": "Логове на връзката",
|
||||||
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
||||||
"httpDestRequestLogsTitle": "Заявки за логове",
|
"httpDestRequestLogsTitle": "Логове за HTTP заявки",
|
||||||
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
||||||
"httpDestSaveChanges": "Запази промените",
|
"httpDestSaveChanges": "Запази промените",
|
||||||
"httpDestCreateDestination": "Създаване на дестинация",
|
"httpDestCreateDestination": "Създаване на дестинация",
|
||||||
@@ -3174,7 +3219,7 @@
|
|||||||
"publicIpEndpoint": "Крайна точка",
|
"publicIpEndpoint": "Крайна точка",
|
||||||
"lastTriggeredAt": "Последен тригер",
|
"lastTriggeredAt": "Последен тригер",
|
||||||
"reject": "Отхвърляне",
|
"reject": "Отхвърляне",
|
||||||
"uptimeDaysAgo": "{count} days ago",
|
"uptimeDaysAgo": "преди {count} дни",
|
||||||
"uptimeToday": "Днес",
|
"uptimeToday": "Днес",
|
||||||
"uptimeNoDataAvailable": "Няма налични данни",
|
"uptimeNoDataAvailable": "Няма налични данни",
|
||||||
"uptimeSuffix": "време без прекъсване",
|
"uptimeSuffix": "време без прекъсване",
|
||||||
@@ -3208,5 +3253,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": "Следващ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
||||||
"shareDeleted": "Odkaz odstraněn",
|
"shareDeleted": "Odkaz odstraněn",
|
||||||
"shareDeletedDescription": "Odkaz byl odstraněn",
|
"shareDeletedDescription": "Odkaz byl odstraněn",
|
||||||
|
"shareDelete": "Smazat odkaz ke sdílení",
|
||||||
|
"shareDeleteConfirm": "Potvrdit smazání odkazu ke sdílení",
|
||||||
|
"shareQuestionRemove": "Jste si jisti, že chcete smazat tento odkaz ke sdílení?",
|
||||||
|
"shareMessageRemove": "Jakmile bude smazán, odkaz přestane fungovat a všichni, kdo jej používají, ztratí přístup k prostředku.",
|
||||||
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.",
|
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.",
|
||||||
"accessToken": "Přístupový token",
|
"accessToken": "Přístupový token",
|
||||||
"usageExamples": "Příklady použití",
|
"usageExamples": "Příklady použití",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.",
|
"userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.",
|
||||||
"userRemoveOrgConfirm": "Potvrdit odebrání uživatele",
|
"userRemoveOrgConfirm": "Potvrdit odebrání uživatele",
|
||||||
"userRemoveOrg": "Odebrat uživatele z organizace",
|
"userRemoveOrg": "Odebrat uživatele z organizace",
|
||||||
|
"userQuestionOrgRemoveSelf": "Jste si jisti, že se chcete odstranit z této organizace?",
|
||||||
|
"userMessageOrgRemoveSelf": "Okamžitě ztratíte přístup. Administrátor vás může později znovu pozvat, ale budete muset přijmout nové pozvání.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Potvrdit odstranění sebe",
|
||||||
|
"userRemoveOrgSelf": "Odstranit se z organizace",
|
||||||
|
"userRemoveOrgSelfWarning": "Okamžitě ztratíte přístup k této organizaci.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ODSTRANIT SE Z ORGANIZACE",
|
||||||
"users": "Uživatelé",
|
"users": "Uživatelé",
|
||||||
"accessRoleMember": "Člen",
|
"accessRoleMember": "Člen",
|
||||||
"accessRoleOwner": "Vlastník",
|
"accessRoleOwner": "Vlastník",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Neplatná e-mailová adresa",
|
"emailInvalid": "Neplatná e-mailová adresa",
|
||||||
"inviteValidityDuration": "Zvolte prosím dobu trvání",
|
"inviteValidityDuration": "Zvolte prosím dobu trvání",
|
||||||
"accessRoleSelectPlease": "Vyberte prosím roli",
|
"accessRoleSelectPlease": "Vyberte prosím roli",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Odebrat přístup správce?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Po uložení již nebudete mít oprávnění správce v této organizaci. Další administrátor vám může přístup obnovit, pokud bude potřeba.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Odebrat mé administrátorské oprávnění",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ODEBRAT MÉ ADMINISTRÁTORSKÉ OPRÁVNĚNÍ",
|
||||||
|
"ownerMustRetainAdminRole": "Vlastník organizace musí zachovat alespoň jednu roli správce.",
|
||||||
"usernameRequired": "Uživatelské jméno je povinné",
|
"usernameRequired": "Uživatelské jméno je povinné",
|
||||||
"idpSelectPlease": "Vyberte poskytovatele identity",
|
"idpSelectPlease": "Vyberte poskytovatele identity",
|
||||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
||||||
"targetsSubmit": "Uložit cíle",
|
"targetsSubmit": "Uložit cíle",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing nebude fungovat mezi lokalitami, které nejsou připojeny ke stejnému uzlu, ale failover bude fungovat.",
|
||||||
"targetErrorInvalidIp": "Neplatná IP adresa",
|
"targetErrorInvalidIp": "Neplatná IP adresa",
|
||||||
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
||||||
"targetErrorInvalidPort": "Neplatný port",
|
"targetErrorInvalidPort": "Neplatný port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Platné heslo",
|
"validPassword": "Platné heslo",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Zobrazit",
|
||||||
|
"configManaged": "Správa konfigurace",
|
||||||
"connectedClient": "Připojený klient",
|
"connectedClient": "Připojený klient",
|
||||||
"resourceBlocked": "Zablokované zdroje",
|
"resourceBlocked": "Zablokované zdroje",
|
||||||
"droppedByRule": "Zrušeno pravidlem",
|
"droppedByRule": "Zrušeno pravidlem",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.",
|
"streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.",
|
||||||
"streamingTypePickerDescription": "Vyberte cílový typ pro začátek.",
|
"streamingTypePickerDescription": "Vyberte cílový typ pro začátek.",
|
||||||
"streamingFailedToLoad": "Nepodařilo se načíst destinace",
|
"streamingLastSyncError": "Došlo k chybě při poslední synchronizaci",
|
||||||
"streamingUnexpectedError": "Došlo k neočekávané chybě.",
|
"streamingUnexpectedError": "Došlo k neočekávané chybě.",
|
||||||
"streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl",
|
"streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl",
|
||||||
"streamingDeletedSuccess": "Cíl byl úspěšně odstraněn",
|
"streamingDeletedSuccess": "Cíl byl úspěšně odstraněn",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Upravit cíl",
|
"S3DestEditTitle": "Upravit cíl",
|
||||||
"S3DestAddTitle": "Přidat S3 cíl",
|
"S3DestAddTitle": "Přidat S3 cíl",
|
||||||
"S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.",
|
"S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.",
|
||||||
"S3DestAddDescription": "Konfigurujte nový S3 koncový bod pro přijímání událostí vaší organizace.",
|
"S3DestAddDescription": "Nakonfigurujte nový Amazon S3 (nebo S3-kompatibilní) bucket, aby přijímal události vaší organizace.",
|
||||||
|
"s3DestTabSettings": "Nastavení",
|
||||||
|
"s3DestTabFormat": "Formát",
|
||||||
|
"s3DestNameLabel": "Jméno",
|
||||||
|
"s3DestNamePlaceholder": "Moje cílové S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID přístupového klíče AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Tajný přístupový klíč AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Váš tajný přístupový klíč AWS",
|
||||||
|
"s3DestRegionLabel": "Oblast AWS",
|
||||||
|
"s3DestBucketLabel": "Název bucketu",
|
||||||
|
"s3DestPrefixLabel": "Předpona klíče (volitelné)",
|
||||||
|
"s3DestPrefixDescription": "Volitelná cesta předpony přidaná ke každému objektovému klíči. Objekty jsou uloženy na {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Vlastní koncový bod (volitelné)",
|
||||||
|
"s3DestEndpointDescription": "Přepište koncový bod S3 pro S3-kompatibilní úložiště, jako je MinIO nebo Cloudflare R2. Nechte prázdné pro standardní AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Komprese Gzip",
|
||||||
|
"s3DestGzipDescription": "Komprimujte každý nahraný objekt pomocí gzip. Snižuje náklady na uložení a velikost nahrávání.",
|
||||||
|
"s3DestFormatTitle": "Formát souboru",
|
||||||
|
"s3DestFormatDescription": "Jak jsou události serializovány v každém nahraném objektu.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Každý objekt je pole JSON záznamů událostí. Kompatibilní s většinou analytických nástrojů.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Každý objekt obsahuje jeden JSON záznam na řádku (newline-delimited JSON). Kompatibilní s Athena, BigQuery a Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Každý objekt je soubor CSV podle RFC-4180 s řádkem záhlaví. Názvy sloupců jsou odvozeny z polí dat událostí.",
|
||||||
|
"s3DestSaveChanges": "Uložit změny",
|
||||||
|
"s3DestCreateDestination": "Vytvořit destinaci",
|
||||||
|
"s3DestUpdatedSuccess": "Destinace úspěšně aktualizována",
|
||||||
|
"s3DestCreatedSuccess": "Destinace úspěšně vytvořena",
|
||||||
|
"s3DestUpdateFailed": "Aktualizace destinace se nezdařila",
|
||||||
|
"s3DestCreateFailed": "Vytvoření destinace se nezdařilo",
|
||||||
"datadogDestEditTitle": "Upravit cíl",
|
"datadogDestEditTitle": "Upravit cíl",
|
||||||
"datadogDestAddTitle": "Přidat Datadog cíl",
|
"datadogDestAddTitle": "Přidat Datadog cíl",
|
||||||
"datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.",
|
"datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.",
|
||||||
@@ -3134,7 +3179,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 +3253,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í"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Fehler beim Löschen des Links",
|
"shareErrorDeleteMessage": "Fehler beim Löschen des Links",
|
||||||
"shareDeleted": "Link gelöscht",
|
"shareDeleted": "Link gelöscht",
|
||||||
"shareDeletedDescription": "Der Link wurde gelöscht",
|
"shareDeletedDescription": "Der Link wurde gelöscht",
|
||||||
|
"shareDelete": "Freigabelink löschen",
|
||||||
|
"shareDeleteConfirm": "Löschen des Freigabelinks bestätigen",
|
||||||
|
"shareQuestionRemove": "Sind Sie sicher, dass Sie diesen Freigabelink löschen möchten?",
|
||||||
|
"shareMessageRemove": "Nach dem Löschen funktioniert der Link nicht mehr, und jeder, der ihn nutzt, verliert den Zugriff auf die Ressource.",
|
||||||
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.",
|
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.",
|
||||||
"accessToken": "Zugangs-Token",
|
"accessToken": "Zugangs-Token",
|
||||||
"usageExamples": "Nutzungsbeispiele",
|
"usageExamples": "Nutzungsbeispiele",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.",
|
"userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.",
|
||||||
"userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen",
|
"userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen",
|
||||||
"userRemoveOrg": "Benutzer aus der Organisation entfernen",
|
"userRemoveOrg": "Benutzer aus der Organisation entfernen",
|
||||||
|
"userQuestionOrgRemoveSelf": "Sind Sie sicher, dass Sie sich aus dieser Organisation entfernen möchten?",
|
||||||
|
"userMessageOrgRemoveSelf": "Sie verlieren sofort den Zugriff. Ein Administrator kann Sie später erneut einladen, aber Sie müssen eine neue Einladung annehmen.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Entfernung bestätigen",
|
||||||
|
"userRemoveOrgSelf": "Sich selbst aus der Organisation entfernen",
|
||||||
|
"userRemoveOrgSelfWarning": "Sie verlieren sofort den Zugriff auf diese Organisation.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ENTFERNUNG MICH SELBST AUS DER ORGANISATION",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"accessRoleMember": "Mitglied",
|
"accessRoleMember": "Mitglied",
|
||||||
"accessRoleOwner": "Eigentümer",
|
"accessRoleOwner": "Eigentümer",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ungültige E-Mail-Adresse",
|
"emailInvalid": "Ungültige E-Mail-Adresse",
|
||||||
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
|
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
|
||||||
"accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
|
"accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Möchten Sie Ihren Administratorzugriff entfernen?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Nach dem Speichern haben Sie keine Administratorrechte mehr in dieser Organisation. Ein anderer Administrator kann den Zugriff bei Bedarf wiederherstellen.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Meinen Administratorzugriff entfernen",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "NIMM MEINEN ADMIN-ZUGRIFF WEG",
|
||||||
|
"ownerMustRetainAdminRole": "Der Organisationsinhaber muss mindestens eine Administratorrolle behalten.",
|
||||||
"usernameRequired": "Benutzername ist erforderlich",
|
"usernameRequired": "Benutzername ist erforderlich",
|
||||||
"idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter",
|
"idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter",
|
||||||
"idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.",
|
"idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
||||||
"targetsSubmit": "Ziele speichern",
|
"targetsSubmit": "Ziele speichern",
|
||||||
"addTarget": "Ziel hinzufügen",
|
"addTarget": "Ziel hinzufügen",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round-Robin-Routing funktioniert nicht zwischen Standorten, die nicht mit demselben Knoten verbunden sind, aber Failover funktioniert.",
|
||||||
"targetErrorInvalidIp": "Ungültige IP-Adresse",
|
"targetErrorInvalidIp": "Ungültige IP-Adresse",
|
||||||
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
|
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
|
||||||
"targetErrorInvalidPort": "Ungültiger Port",
|
"targetErrorInvalidPort": "Ungültiger Port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Gültiges Passwort",
|
"validPassword": "Gültiges Passwort",
|
||||||
"validEmail": "Gültige E-Mail-Adresse",
|
"validEmail": "Gültige E-Mail-Adresse",
|
||||||
"validSSO": "Gültige SSO-Anmeldung",
|
"validSSO": "Gültige SSO-Anmeldung",
|
||||||
|
"view": "Ansehen",
|
||||||
|
"configManaged": "Konfiguration verwaltet",
|
||||||
"connectedClient": "Verbundenes Gerät",
|
"connectedClient": "Verbundenes Gerät",
|
||||||
"resourceBlocked": "Ressource blockiert",
|
"resourceBlocked": "Ressource blockiert",
|
||||||
"droppedByRule": "Abgelegt durch Regel",
|
"droppedByRule": "Abgelegt durch Regel",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
||||||
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
||||||
"streamingFailedToLoad": "Fehler beim Laden der Ziele",
|
"streamingLastSyncError": "Beim letzten Synchronisieren ist ein Fehler aufgetreten.",
|
||||||
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||||
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
||||||
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Ziel bearbeiten",
|
"S3DestEditTitle": "Ziel bearbeiten",
|
||||||
"S3DestAddTitle": "S3-Ziel hinzufügen",
|
"S3DestAddTitle": "S3-Ziel hinzufügen",
|
||||||
"S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.",
|
"S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.",
|
||||||
"S3DestAddDescription": "Neuen S3-Endpunkt konfigurieren, um die Ereignisse Ihrer Organisation zu erhalten.",
|
"S3DestAddDescription": "Konfigurieren Sie einen neuen Amazon S3 (oder S3-kompatiblen) Bucket, um die Ereignisse Ihrer Organisation zu empfangen.",
|
||||||
|
"s3DestTabSettings": "Einstellungen",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Name",
|
||||||
|
"s3DestNamePlaceholder": "Mein S3-Ziel",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS-Zugriffsschlüssel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS-Geheimzugriffsschlüssel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Ihr AWS-Geheimzugriffsschlüssel",
|
||||||
|
"s3DestRegionLabel": "AWS-Region",
|
||||||
|
"s3DestBucketLabel": "Bucket-Name",
|
||||||
|
"s3DestPrefixLabel": "Schlüssel-Präfix (optional)",
|
||||||
|
"s3DestPrefixDescription": "Optionales Pfadpräfix, das jedem Objektschlüssel vorangestellt wird. Objekte werden unter {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} gespeichert.",
|
||||||
|
"s3DestEndpointLabel": "Benutzerdefinierter Endpunkt (optional)",
|
||||||
|
"s3DestEndpointDescription": "Überschreiben Sie den S3-Endpunkt für S3-kompatiblen Speicher wie MinIO oder Cloudflare R2. Lassen Sie das Feld leer für standardmäßiges AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-Komprimierung",
|
||||||
|
"s3DestGzipDescription": "Jedes hochgeladene Objekt mit Gzip komprimieren. Reduziert die Speicherkosten und die Upload-Größe.",
|
||||||
|
"s3DestFormatTitle": "Dateiformat",
|
||||||
|
"s3DestFormatDescription": "Wie Ereignisse in jedem hochgeladenen Objekt serialisiert werden.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Jedes Objekt ist ein JSON-Array von Ereignisdaten. Kompatibel mit den meisten Analysetools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Jedes Objekt enthält einen JSON-Datensatz pro Zeile (newline-delimited JSON). Kompatibel mit Athena, BigQuery und Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Jedes Objekt ist eine RFC-4180 CSV-Datei mit einer Kopfzeile. Spaltennamen werden aus den Ereignisdatenfeldern abgeleitet.",
|
||||||
|
"s3DestSaveChanges": "Änderungen speichern",
|
||||||
|
"s3DestCreateDestination": "Ziel erstellen",
|
||||||
|
"s3DestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
|
||||||
|
"s3DestCreatedSuccess": "Ziel erfolgreich erstellt",
|
||||||
|
"s3DestUpdateFailed": "Fehler beim Aktualisieren des Ziels",
|
||||||
|
"s3DestCreateFailed": "Fehler beim Erstellen des Ziels",
|
||||||
"datadogDestEditTitle": "Ziel bearbeiten",
|
"datadogDestEditTitle": "Ziel bearbeiten",
|
||||||
"datadogDestAddTitle": "Datadog-Ziel hinzufügen",
|
"datadogDestAddTitle": "Datadog-Ziel hinzufügen",
|
||||||
"datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.",
|
"datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
"shareErrorDeleteMessage": "An error occurred deleting link",
|
||||||
"shareDeleted": "Link deleted",
|
"shareDeleted": "Link deleted",
|
||||||
"shareDeletedDescription": "The link has been deleted",
|
"shareDeletedDescription": "The link has been deleted",
|
||||||
|
"shareDelete": "Delete Share Link",
|
||||||
|
"shareDeleteConfirm": "Confirm Delete Share Link",
|
||||||
|
"shareQuestionRemove": "Are you sure you want to delete this share link?",
|
||||||
|
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
|
||||||
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"usageExamples": "Usage Examples",
|
"usageExamples": "Usage Examples",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||||
"userRemoveOrg": "Remove User from Organization",
|
"userRemoveOrg": "Remove User from Organization",
|
||||||
|
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||||
|
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||||
|
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||||
|
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"accessRoleMember": "Member",
|
"accessRoleMember": "Member",
|
||||||
"accessRoleOwner": "Owner",
|
"accessRoleOwner": "Owner",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Invalid email address",
|
"emailInvalid": "Invalid email address",
|
||||||
"inviteValidityDuration": "Please select a duration",
|
"inviteValidityDuration": "Please select a duration",
|
||||||
"accessRoleSelectPlease": "Please select a role",
|
"accessRoleSelectPlease": "Please select a role",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||||
|
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||||
"usernameRequired": "Username is required",
|
"usernameRequired": "Username is required",
|
||||||
"idpSelectPlease": "Please select an identity provider",
|
"idpSelectPlease": "Please select an identity provider",
|
||||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"targetsSubmit": "Save Targets",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.",
|
||||||
"targetErrorInvalidIp": "Invalid IP address",
|
"targetErrorInvalidIp": "Invalid IP address",
|
||||||
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
||||||
"targetErrorInvalidPort": "Invalid port",
|
"targetErrorInvalidPort": "Invalid port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Valid Password",
|
"validPassword": "Valid Password",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "View",
|
||||||
|
"configManaged": "Config Managed",
|
||||||
"connectedClient": "Connected Client",
|
"connectedClient": "Connected Client",
|
||||||
"resourceBlocked": "Resource Blocked",
|
"resourceBlocked": "Resource Blocked",
|
||||||
"droppedByRule": "Dropped by Rule",
|
"droppedByRule": "Dropped by Rule",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
||||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||||
"streamingFailedToLoad": "Failed to load destinations",
|
"streamingLastSyncError": "An error occurred on the last sync",
|
||||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||||
"streamingFailedToUpdate": "Failed to update destination",
|
"streamingFailedToUpdate": "Failed to update destination",
|
||||||
"streamingDeletedSuccess": "Destination deleted successfully",
|
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Edit Destination",
|
"S3DestEditTitle": "Edit Destination",
|
||||||
"S3DestAddTitle": "Add S3 Destination",
|
"S3DestAddTitle": "Add S3 Destination",
|
||||||
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
||||||
"S3DestAddDescription": "Configure a new S3 endpoint to receive your organization's events.",
|
"S3DestAddDescription": "Configure a new Amazon S3 (or S3-compatible) bucket to receive your organization's events.",
|
||||||
|
"s3DestTabSettings": "Settings",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Name",
|
||||||
|
"s3DestNamePlaceholder": "My S3 destination",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Your AWS secret access key",
|
||||||
|
"s3DestRegionLabel": "AWS Region",
|
||||||
|
"s3DestBucketLabel": "Bucket Name",
|
||||||
|
"s3DestPrefixLabel": "Key Prefix (optional)",
|
||||||
|
"s3DestPrefixDescription": "Optional path prefix prepended to every object key. Objects are stored at {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Custom Endpoint (optional)",
|
||||||
|
"s3DestEndpointDescription": "Override the S3 endpoint for S3-compatible storage such as MinIO or Cloudflare R2. Leave blank for standard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip compression",
|
||||||
|
"s3DestGzipDescription": "Compress each uploaded object with gzip. Reduces storage costs and upload size.",
|
||||||
|
"s3DestFormatTitle": "File Format",
|
||||||
|
"s3DestFormatDescription": "How events are serialised inside each uploaded object.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Each object is a JSON array of event records. Compatible with most analytics tools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Each object contains one JSON record per line (newline-delimited JSON). Compatible with Athena, BigQuery, and Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Each object is an RFC-4180 CSV file with a header row. Column names are derived from the event data fields.",
|
||||||
|
"s3DestSaveChanges": "Save Changes",
|
||||||
|
"s3DestCreateDestination": "Create Destination",
|
||||||
|
"s3DestUpdatedSuccess": "Destination updated successfully",
|
||||||
|
"s3DestCreatedSuccess": "Destination created successfully",
|
||||||
|
"s3DestUpdateFailed": "Failed to update destination",
|
||||||
|
"s3DestCreateFailed": "Failed to create destination",
|
||||||
"datadogDestEditTitle": "Edit Destination",
|
"datadogDestEditTitle": "Edit Destination",
|
||||||
"datadogDestAddTitle": "Add Datadog Destination",
|
"datadogDestAddTitle": "Add Datadog Destination",
|
||||||
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
||||||
@@ -3134,7 +3179,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",
|
||||||
@@ -3208,5 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace",
|
"shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace",
|
||||||
"shareDeleted": "Enlace eliminado",
|
"shareDeleted": "Enlace eliminado",
|
||||||
"shareDeletedDescription": "El enlace ha sido eliminado",
|
"shareDeletedDescription": "El enlace ha sido eliminado",
|
||||||
|
"shareDelete": "Borrar Enlace Compartido",
|
||||||
|
"shareDeleteConfirm": "Confirmar Borrado del Enlace Compartido",
|
||||||
|
"shareQuestionRemove": "¿Está seguro de que desea borrar este enlace compartido?",
|
||||||
|
"shareMessageRemove": "Una vez borrado, el enlace dejará de funcionar y cualquier persona que lo use perderá acceso al recurso.",
|
||||||
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.",
|
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.",
|
||||||
"accessToken": "Token de acceso",
|
"accessToken": "Token de acceso",
|
||||||
"usageExamples": "Ejemplos de uso",
|
"usageExamples": "Ejemplos de uso",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.",
|
"userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.",
|
||||||
"userRemoveOrgConfirm": "Confirmar eliminar usuario",
|
"userRemoveOrgConfirm": "Confirmar eliminar usuario",
|
||||||
"userRemoveOrg": "Eliminar usuario de la organización",
|
"userRemoveOrg": "Eliminar usuario de la organización",
|
||||||
|
"userQuestionOrgRemoveSelf": "¿Está seguro de que desea eliminarse de esta organización?",
|
||||||
|
"userMessageOrgRemoveSelf": "Perderá acceso inmediatamente. Un administrador puede invitarlo de nuevo más tarde, pero necesitará aceptar una nueva invitación.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmar Eliminarme",
|
||||||
|
"userRemoveOrgSelf": "Eliminarse de la organización",
|
||||||
|
"userRemoveOrgSelfWarning": "Perderá acceso a esta organización inmediatamente.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ELIMINARME DE LA ORGANIZACIÓN",
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
"accessRoleMember": "Miembro",
|
"accessRoleMember": "Miembro",
|
||||||
"accessRoleOwner": "Propietario",
|
"accessRoleOwner": "Propietario",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Dirección de correo inválida",
|
"emailInvalid": "Dirección de correo inválida",
|
||||||
"inviteValidityDuration": "Por favor, seleccione una duración",
|
"inviteValidityDuration": "Por favor, seleccione una duración",
|
||||||
"accessRoleSelectPlease": "Por favor, seleccione un rol",
|
"accessRoleSelectPlease": "Por favor, seleccione un rol",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "¿Eliminar su acceso de administrador?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Ya no tendrá permisos de administrador en esta organización después de guardar. Otro administrador puede restaurar el acceso si es necesario.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Eliminar Mi Acceso de Administrador",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ELIMINAR MI ACCESO DE ADMINISTRADOR",
|
||||||
|
"ownerMustRetainAdminRole": "El propietario de la organización debe mantener al menos un rol de administrador.",
|
||||||
"usernameRequired": "Nombre de usuario requerido",
|
"usernameRequired": "Nombre de usuario requerido",
|
||||||
"idpSelectPlease": "Por favor, seleccione un proveedor de identidad",
|
"idpSelectPlease": "Por favor, seleccione un proveedor de identidad",
|
||||||
"idpGenericOidc": "Proveedor OAuth2/OIDC genérico.",
|
"idpGenericOidc": "Proveedor OAuth2/OIDC genérico.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
||||||
"targetsSubmit": "Guardar objetivos",
|
"targetsSubmit": "Guardar objetivos",
|
||||||
"addTarget": "Añadir destino",
|
"addTarget": "Añadir destino",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "El enrutamiento de turnos no funcionará entre sitios que no están conectados al mismo nodo, pero el failover funcionará.",
|
||||||
"targetErrorInvalidIp": "Dirección IP inválida",
|
"targetErrorInvalidIp": "Dirección IP inválida",
|
||||||
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
|
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
|
||||||
"targetErrorInvalidPort": "Puerto inválido",
|
"targetErrorInvalidPort": "Puerto inválido",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Contraseña válida",
|
"validPassword": "Contraseña válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Ver",
|
||||||
|
"configManaged": "Configuración Gestionada",
|
||||||
"connectedClient": "Cliente conectado",
|
"connectedClient": "Cliente conectado",
|
||||||
"resourceBlocked": "Recurso bloqueado",
|
"resourceBlocked": "Recurso bloqueado",
|
||||||
"droppedByRule": "Soltado por regla",
|
"droppedByRule": "Soltado por regla",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.",
|
"streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.",
|
||||||
"streamingTypePickerDescription": "Elija un tipo de destino para empezar.",
|
"streamingTypePickerDescription": "Elija un tipo de destino para empezar.",
|
||||||
"streamingFailedToLoad": "Error al cargar destinos",
|
"streamingLastSyncError": "Ocurrió un error en la última sincronización.",
|
||||||
"streamingUnexpectedError": "Se ha producido un error inesperado.",
|
"streamingUnexpectedError": "Se ha producido un error inesperado.",
|
||||||
"streamingFailedToUpdate": "Error al actualizar destino",
|
"streamingFailedToUpdate": "Error al actualizar destino",
|
||||||
"streamingDeletedSuccess": "Destino eliminado correctamente",
|
"streamingDeletedSuccess": "Destino eliminado correctamente",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Editar destino",
|
"S3DestEditTitle": "Editar destino",
|
||||||
"S3DestAddTitle": "Añadir destino S3",
|
"S3DestAddTitle": "Añadir destino S3",
|
||||||
"S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.",
|
"S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.",
|
||||||
"S3DestAddDescription": "Configure un nuevo punto final S3 para recibir los eventos de su organización.",
|
"S3DestAddDescription": "Configura un nuevo bucket de Amazon S3 (o compatible con S3) para recibir los eventos de tu organización.",
|
||||||
|
"s3DestTabSettings": "Ajustes",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nombre",
|
||||||
|
"s3DestNamePlaceholder": "Mi destino S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID de clave de acceso de AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Clave de acceso secreta de AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Tu clave de acceso secreta de AWS",
|
||||||
|
"s3DestRegionLabel": "Región de AWS",
|
||||||
|
"s3DestBucketLabel": "Nombre del bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefijo clave (opcional)",
|
||||||
|
"s3DestPrefixDescription": "Prefijo de ruta opcional preanexado a cada clave de objeto. Los objetos se almacenan en {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Punto final personalizado (opcional)",
|
||||||
|
"s3DestEndpointDescription": "Sobrescribe el punto final de S3 para almacenamiento compatible con S3 como MinIO o Cloudflare R2. Deja en blanco para el estándar AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Compresión Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprime cada objeto subido con gzip. Reduce costos de almacenamiento y tamaño de carga.",
|
||||||
|
"s3DestFormatTitle": "Formato de archivo",
|
||||||
|
"s3DestFormatDescription": "Cómo se serializan los eventos dentro de cada objeto cargado.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Cada objeto es un arreglo JSON de registros de eventos. Compatible con la mayoría de las herramientas de analítica.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Cada objeto contiene un registro JSON por línea (JSON delimitado por nueva línea). Compatible con Athena, BigQuery y Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Cada objeto es un archivo CSV conforme a RFC-4180 con una fila de encabezado. Los nombres de columna se derivan de los campos de datos del evento.",
|
||||||
|
"s3DestSaveChanges": "Guardar cambios",
|
||||||
|
"s3DestCreateDestination": "Crear destino",
|
||||||
|
"s3DestUpdatedSuccess": "Destino actualizado con éxito",
|
||||||
|
"s3DestCreatedSuccess": "Destino creado con éxito",
|
||||||
|
"s3DestUpdateFailed": "No se pudo actualizar el destino",
|
||||||
|
"s3DestCreateFailed": "No se pudo crear el destino",
|
||||||
"datadogDestEditTitle": "Editar destino",
|
"datadogDestEditTitle": "Editar destino",
|
||||||
"datadogDestAddTitle": "Añadir destino Datadog",
|
"datadogDestAddTitle": "Añadir destino Datadog",
|
||||||
"datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.",
|
"datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
||||||
"shareDeleted": "Lien supprimé",
|
"shareDeleted": "Lien supprimé",
|
||||||
"shareDeletedDescription": "Le lien a été supprimé",
|
"shareDeletedDescription": "Le lien a été supprimé",
|
||||||
|
"shareDelete": "Supprimer le lien de partage",
|
||||||
|
"shareDeleteConfirm": "Confirmer la suppression du lien de partage",
|
||||||
|
"shareQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce lien de partage ?",
|
||||||
|
"shareMessageRemove": "Une fois supprimé, le lien ne fonctionnera plus et toute personne l'utilisant perdra l'accès à la ressource.",
|
||||||
"shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
"shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
||||||
"accessToken": "Jeton d'accès",
|
"accessToken": "Jeton d'accès",
|
||||||
"usageExamples": "Exemples d'utilisation",
|
"usageExamples": "Exemples d'utilisation",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.",
|
"userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.",
|
||||||
"userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur",
|
"userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur",
|
||||||
"userRemoveOrg": "Retirer l'utilisateur de l'organisation",
|
"userRemoveOrg": "Retirer l'utilisateur de l'organisation",
|
||||||
|
"userQuestionOrgRemoveSelf": "Êtes-vous sûr de vouloir vous retirer de cette organisation ?",
|
||||||
|
"userMessageOrgRemoveSelf": "Vous perdrez immédiatement l'accès. Un administrateur pourra vous inviter à nouveau plus tard, mais vous devrez accepter une nouvelle invitation.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmer la suppression de moi-même",
|
||||||
|
"userRemoveOrgSelf": "Se retirer de l'organisation",
|
||||||
|
"userRemoveOrgSelfWarning": "Vous perdrez immédiatement l'accès à cette organisation.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "SUPPRIMER MOI-MÊME DE L'ORG",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"accessRoleMember": "Membre",
|
"accessRoleMember": "Membre",
|
||||||
"accessRoleOwner": "Propriétaire",
|
"accessRoleOwner": "Propriétaire",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Adresse e-mail invalide",
|
"emailInvalid": "Adresse e-mail invalide",
|
||||||
"inviteValidityDuration": "Veuillez sélectionner une durée",
|
"inviteValidityDuration": "Veuillez sélectionner une durée",
|
||||||
"accessRoleSelectPlease": "Veuillez sélectionner un rôle",
|
"accessRoleSelectPlease": "Veuillez sélectionner un rôle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Retirer votre accès administrateur ?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Vous n'aurez plus de droits d'administrateur dans cette organisation après avoir enregistré. Un autre administrateur pourra restaurer cet accès si nécessaire.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Retirer mon accès administrateur",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "RETIRER MON ACCÈS ADMIN",
|
||||||
|
"ownerMustRetainAdminRole": "Le propriétaire de l'organisation doit conserver au moins un rôle d'administrateur.",
|
||||||
"usernameRequired": "Le nom d'utilisateur est requis",
|
"usernameRequired": "Le nom d'utilisateur est requis",
|
||||||
"idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité",
|
"idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité",
|
||||||
"idpGenericOidc": "Fournisseur OAuth2/OIDC générique.",
|
"idpGenericOidc": "Fournisseur OAuth2/OIDC générique.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
||||||
"targetsSubmit": "Enregistrer les cibles",
|
"targetsSubmit": "Enregistrer les cibles",
|
||||||
"addTarget": "Ajouter une cible",
|
"addTarget": "Ajouter une cible",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Le routage en tourniquet n'opérera pas entre des sites qui ne sont pas connectés au même nœud, mais le basculement fonctionnera.",
|
||||||
"targetErrorInvalidIp": "Adresse IP invalide",
|
"targetErrorInvalidIp": "Adresse IP invalide",
|
||||||
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
|
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
|
||||||
"targetErrorInvalidPort": "Port invalide",
|
"targetErrorInvalidPort": "Port invalide",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Mot de passe valide",
|
"validPassword": "Mot de passe valide",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Afficher",
|
||||||
|
"configManaged": "Configuration gérée",
|
||||||
"connectedClient": "Client connecté",
|
"connectedClient": "Client connecté",
|
||||||
"resourceBlocked": "Ressource bloquée",
|
"resourceBlocked": "Ressource bloquée",
|
||||||
"droppedByRule": "Abandonné par la règle",
|
"droppedByRule": "Abandonné par la règle",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.",
|
"streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.",
|
||||||
"streamingTypePickerDescription": "Choisissez un type de destination pour commencer.",
|
"streamingTypePickerDescription": "Choisissez un type de destination pour commencer.",
|
||||||
"streamingFailedToLoad": "Impossible de charger les destinations",
|
"streamingLastSyncError": "Une erreur s'est produite lors de la dernière synchronisation",
|
||||||
"streamingUnexpectedError": "Une erreur inattendue s'est produite.",
|
"streamingUnexpectedError": "Une erreur inattendue s'est produite.",
|
||||||
"streamingFailedToUpdate": "Impossible de mettre à jour la destination",
|
"streamingFailedToUpdate": "Impossible de mettre à jour la destination",
|
||||||
"streamingDeletedSuccess": "Destination supprimée avec succès",
|
"streamingDeletedSuccess": "Destination supprimée avec succès",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Modifier la destination",
|
"S3DestEditTitle": "Modifier la destination",
|
||||||
"S3DestAddTitle": "Ajouter une destination S3",
|
"S3DestAddTitle": "Ajouter une destination S3",
|
||||||
"S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.",
|
"S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.",
|
||||||
"S3DestAddDescription": "Configurer un nouveau point de terminaison S3 pour recevoir les événements de votre organisation.",
|
"S3DestAddDescription": "Configurez un nouveau bucket Amazon S3 (ou compatible S3) pour recevoir les événements de votre organisation.",
|
||||||
|
"s3DestTabSettings": "Réglages",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Nom",
|
||||||
|
"s3DestNamePlaceholder": "Ma destination S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID de clé d'accès AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Clé d'accès secrète AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Votre clé d'accès secrète AWS",
|
||||||
|
"s3DestRegionLabel": "Région AWS",
|
||||||
|
"s3DestBucketLabel": "Nom du bucket",
|
||||||
|
"s3DestPrefixLabel": "Préfixe clé (facultatif)",
|
||||||
|
"s3DestPrefixDescription": "Préfixe de chemin facultatif préfixé à chaque clé d'objet. Les objets sont stockés à {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Point de terminaison personnalisé (facultatif)",
|
||||||
|
"s3DestEndpointDescription": "Modifiez le point de terminaison S3 pour un stockage compatible S3 tel que MinIO ou Cloudflare R2. Laissez vide pour l'AWS S3 standard.",
|
||||||
|
"s3DestGzipLabel": "Compression Gzip",
|
||||||
|
"s3DestGzipDescription": "Compressez chaque objet téléchargé avec gzip. Réduit les coûts de stockage et la taille de téléchargement.",
|
||||||
|
"s3DestFormatTitle": "Format de fichier",
|
||||||
|
"s3DestFormatDescription": "Comment les événements sont sérialisés dans chaque objet téléchargé.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Chaque objet est un tableau JSON des enregistrements d'événements. Compatible avec la plupart des outils d'analyse.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Chaque objet contient un enregistrement JSON par ligne (JSON délimité par saut de ligne). Compatible avec Athena, BigQuery et Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Chaque objet est un fichier CSV RFC-4180 avec une ligne d'en-tête. Les noms de colonne sont dérivés des champs de données de l'événement.",
|
||||||
|
"s3DestSaveChanges": "Enregistrer les modifications",
|
||||||
|
"s3DestCreateDestination": "Créer une destination",
|
||||||
|
"s3DestUpdatedSuccess": "Destination mise à jour avec succès",
|
||||||
|
"s3DestCreatedSuccess": "Destination créée avec succès",
|
||||||
|
"s3DestUpdateFailed": "Échec de la mise à jour de la destination",
|
||||||
|
"s3DestCreateFailed": "Échec de la création de la destination",
|
||||||
"datadogDestEditTitle": "Modifier la destination",
|
"datadogDestEditTitle": "Modifier la destination",
|
||||||
"datadogDestAddTitle": "Ajouter une destination Datadog",
|
"datadogDestAddTitle": "Ajouter une destination Datadog",
|
||||||
"datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.",
|
"datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.",
|
||||||
@@ -3134,7 +3179,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",
|
||||||
@@ -3208,5 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione del link",
|
"shareErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione del link",
|
||||||
"shareDeleted": "Link eliminato",
|
"shareDeleted": "Link eliminato",
|
||||||
"shareDeletedDescription": "Il link è stato eliminato",
|
"shareDeletedDescription": "Il link è stato eliminato",
|
||||||
|
"shareDelete": "Elimina Link di Condivisione",
|
||||||
|
"shareDeleteConfirm": "Conferma Eliminazione Link di Condivisione",
|
||||||
|
"shareQuestionRemove": "Sei sicuro di voler eliminare questo link di condivisione?",
|
||||||
|
"shareMessageRemove": "Una volta eliminato, il link non funzionerà più e chiunque lo utilizzi perderà l'accesso alla risorsa.",
|
||||||
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.",
|
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.",
|
||||||
"accessToken": "Token Di Accesso",
|
"accessToken": "Token Di Accesso",
|
||||||
"usageExamples": "Esempi Di Utilizzo",
|
"usageExamples": "Esempi Di Utilizzo",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||||
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||||
|
"userQuestionOrgRemoveSelf": "Sei sicuro di voler rimuovere te stesso da questa organizzazione?",
|
||||||
|
"userMessageOrgRemoveSelf": "Perderai immediatamente l'accesso. Un amministratore può invitarti nuovamente in seguito, ma dovrai accettare un nuovo invito.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Conferma Rimozione Me Stesso",
|
||||||
|
"userRemoveOrgSelf": "Rimuoviti dall'organizzazione",
|
||||||
|
"userRemoveOrgSelfWarning": "Perderai immediatamente l'accesso a questa organizzazione.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "RIMUOVITI DALL'ORGANIZZAZIONE",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
"accessRoleMember": "Membro",
|
"accessRoleMember": "Membro",
|
||||||
"accessRoleOwner": "Proprietario",
|
"accessRoleOwner": "Proprietario",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Indirizzo email non valido",
|
"emailInvalid": "Indirizzo email non valido",
|
||||||
"inviteValidityDuration": "Seleziona una durata",
|
"inviteValidityDuration": "Seleziona una durata",
|
||||||
"accessRoleSelectPlease": "Seleziona un ruolo",
|
"accessRoleSelectPlease": "Seleziona un ruolo",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Rimuovere il tuo accesso amministrativo?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Non avrai più i permessi di amministratore in questa organizzazione dopo il salvataggio. Un altro amministratore può ripristinare l'accesso se necessario.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Rimuovere il Mio Accesso Amministrativo",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "RIMUOVERE IL MIO ACCESSO AMMINISTRATIVO",
|
||||||
|
"ownerMustRetainAdminRole": "Il proprietario dell'organizzazione deve mantenere almeno un ruolo di amministratore.",
|
||||||
"usernameRequired": "Username richiesto",
|
"usernameRequired": "Username richiesto",
|
||||||
"idpSelectPlease": "Seleziona un provider di identità",
|
"idpSelectPlease": "Seleziona un provider di identità",
|
||||||
"idpGenericOidc": "Provider OAuth2/OIDC generico.",
|
"idpGenericOidc": "Provider OAuth2/OIDC generico.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
||||||
"targetsSubmit": "Salva Target",
|
"targetsSubmit": "Salva Target",
|
||||||
"addTarget": "Aggiungi Target",
|
"addTarget": "Aggiungi Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Il routing round robin non funzionerà tra siti che non sono connessi allo stesso nodo, ma il failover funzionerà.",
|
||||||
"targetErrorInvalidIp": "Indirizzo IP non valido",
|
"targetErrorInvalidIp": "Indirizzo IP non valido",
|
||||||
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
|
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
|
||||||
"targetErrorInvalidPort": "Porta non valida",
|
"targetErrorInvalidPort": "Porta non valida",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Password Valida",
|
"validPassword": "Password Valida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Visualizza",
|
||||||
|
"configManaged": "Gestione Configurazione",
|
||||||
"connectedClient": "Cliente Connesso",
|
"connectedClient": "Cliente Connesso",
|
||||||
"resourceBlocked": "Risorsa Bloccata",
|
"resourceBlocked": "Risorsa Bloccata",
|
||||||
"droppedByRule": "Eliminato dalla regola",
|
"droppedByRule": "Eliminato dalla regola",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.",
|
"streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.",
|
||||||
"streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.",
|
"streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.",
|
||||||
"streamingFailedToLoad": "Impossibile caricare le destinazioni",
|
"streamingLastSyncError": "Si è verificato un errore durante l'ultima sincronizzazione",
|
||||||
"streamingUnexpectedError": "Si è verificato un errore imprevisto.",
|
"streamingUnexpectedError": "Si è verificato un errore imprevisto.",
|
||||||
"streamingFailedToUpdate": "Impossibile aggiornare la destinazione",
|
"streamingFailedToUpdate": "Impossibile aggiornare la destinazione",
|
||||||
"streamingDeletedSuccess": "Destinazione eliminata con successo",
|
"streamingDeletedSuccess": "Destinazione eliminata con successo",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Modifica Destinazione",
|
"S3DestEditTitle": "Modifica Destinazione",
|
||||||
"S3DestAddTitle": "Aggiungi Destinazione S3",
|
"S3DestAddTitle": "Aggiungi Destinazione S3",
|
||||||
"S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.",
|
"S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.",
|
||||||
"S3DestAddDescription": "Configura un nuovo endpoint S3 per ricevere gli eventi della tua organizzazione.",
|
"S3DestAddDescription": "Configura un nuovo bucket Amazon S3 (o compatibile con S3) per ricevere gli eventi della tua organizzazione.",
|
||||||
|
"s3DestTabSettings": "Impostazioni",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nome",
|
||||||
|
"s3DestNamePlaceholder": "La mia destinazione S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID Chiave Accesso AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Chiave Segreta Accesso AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "La tua chiave segreta di accesso AWS",
|
||||||
|
"s3DestRegionLabel": "Regione AWS",
|
||||||
|
"s3DestBucketLabel": "Nome Bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefisso Chiave (facoltativo)",
|
||||||
|
"s3DestPrefixDescription": "Prefisso percorso facoltativo anteposto a ogni chiave oggetto. Gli oggetti vengono archiviati in {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Endpoint personalizzato (facoltativo)",
|
||||||
|
"s3DestEndpointDescription": "Sostituisci l'endpoint S3 per lo storage compatibile con S3 come MinIO o Cloudflare R2. Lasciare vuoto per l'AWS S3 standard.",
|
||||||
|
"s3DestGzipLabel": "Compressione Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprimi ogni oggetto caricato con gzip. Riduce i costi di archiviazione e la dimensione di caricamento.",
|
||||||
|
"s3DestFormatTitle": "Formato del File",
|
||||||
|
"s3DestFormatDescription": "Come gli eventi sono serializzati all'interno di ciascun oggetto caricato.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Ogni oggetto è un array JSON di record di eventi. Compatibile con la maggior parte degli strumenti analitici.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Ogni oggetto contiene un record JSON per linea (JSON delimitato da newline). Compatibile con Athena, BigQuery e Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "\"CSV\"",
|
||||||
|
"s3DestFormatCsvDescription": "Ogni oggetto è un file CSV RFC-4180 con una riga di intestazione. I nomi delle colonne sono derivati dai campi dei dati degli eventi.",
|
||||||
|
"s3DestSaveChanges": "Salva modifiche",
|
||||||
|
"s3DestCreateDestination": "Crea destinazione",
|
||||||
|
"s3DestUpdatedSuccess": "Destinazione aggiornata con successo",
|
||||||
|
"s3DestCreatedSuccess": "Destinazione creata con successo",
|
||||||
|
"s3DestUpdateFailed": "Aggiornamento della destinazione fallito",
|
||||||
|
"s3DestCreateFailed": "Creazione della destinazione fallita",
|
||||||
"datadogDestEditTitle": "Modifica Destinazione",
|
"datadogDestEditTitle": "Modifica Destinazione",
|
||||||
"datadogDestAddTitle": "Aggiungi Destinazione Datadog",
|
"datadogDestAddTitle": "Aggiungi Destinazione Datadog",
|
||||||
"datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.",
|
"datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.",
|
"shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.",
|
||||||
"shareDeleted": "링크가 삭제되었습니다.",
|
"shareDeleted": "링크가 삭제되었습니다.",
|
||||||
"shareDeletedDescription": "링크가 삭제되었습니다.",
|
"shareDeletedDescription": "링크가 삭제되었습니다.",
|
||||||
|
"shareDelete": "공유 링크 삭제",
|
||||||
|
"shareDeleteConfirm": "공유 링크 삭제 확인",
|
||||||
|
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
|
||||||
|
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
|
||||||
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
|
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
|
||||||
"accessToken": "액세스 토큰",
|
"accessToken": "액세스 토큰",
|
||||||
"usageExamples": "사용 예",
|
"usageExamples": "사용 예",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
||||||
"userRemoveOrgConfirm": "사용자 제거 확인",
|
"userRemoveOrgConfirm": "사용자 제거 확인",
|
||||||
"userRemoveOrg": "조직에서 사용자 제거",
|
"userRemoveOrg": "조직에서 사용자 제거",
|
||||||
|
"userQuestionOrgRemoveSelf": "이 조직에서 자신을 제거하시겠습니까?",
|
||||||
|
"userMessageOrgRemoveSelf": "귀하는 즉시 접근 권한을 잃게 됩니다. 관리자가 나중에 다시 초대할 수 있지만, 새 초대를 수락해야 합니다.",
|
||||||
|
"userRemoveOrgConfirmSelf": "내 제거 확인",
|
||||||
|
"userRemoveOrgSelf": "조직에서 자신을 제거하십시오",
|
||||||
|
"userRemoveOrgSelfWarning": "귀하는 이 조직에 대한 접근 권한을 즉시 상실합니다.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "조직에서 나를 제거",
|
||||||
"users": "사용자",
|
"users": "사용자",
|
||||||
"accessRoleMember": "회원",
|
"accessRoleMember": "회원",
|
||||||
"accessRoleOwner": "소유자",
|
"accessRoleOwner": "소유자",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
|
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
|
||||||
"inviteValidityDuration": "지속 시간을 선택하십시오.",
|
"inviteValidityDuration": "지속 시간을 선택하십시오.",
|
||||||
"accessRoleSelectPlease": "역할을 선택하세요",
|
"accessRoleSelectPlease": "역할을 선택하세요",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "관리자 권한을 제거하시겠습니까?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "저장 후 이 조직에 대한 관리자 권한이 없어집니다. 필요한 경우 다른 관리자가 접근 권한을 복구할 수 있습니다.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "내 관리자 권한 제거",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "내 관리자 권한 제거",
|
||||||
|
"ownerMustRetainAdminRole": "조직 소유자는 최소한 하나의 관리자 역할을 유지해야 합니다.",
|
||||||
"usernameRequired": "사용자 이름은 필수입니다.",
|
"usernameRequired": "사용자 이름은 필수입니다.",
|
||||||
"idpSelectPlease": "신원 제공자를 선택하십시오",
|
"idpSelectPlease": "신원 제공자를 선택하십시오",
|
||||||
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
|
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||||
"targetsSubmit": "대상 저장",
|
"targetsSubmit": "대상 저장",
|
||||||
"addTarget": "대상 추가",
|
"addTarget": "대상 추가",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "라운드 로빈 라우팅은 동일한 노드에 연결되지 않은 사이트 간에는 작동하지 않으나, 대체 라우팅은 작동합니다.",
|
||||||
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||||
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||||
"targetErrorInvalidPort": "유효하지 않은 포트",
|
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "유효한 비밀번호",
|
"validPassword": "유효한 비밀번호",
|
||||||
"validEmail": "유효한 이메일",
|
"validEmail": "유효한 이메일",
|
||||||
"validSSO": "유효한 SSO",
|
"validSSO": "유효한 SSO",
|
||||||
|
"view": "보기",
|
||||||
|
"configManaged": "구성 관리됨",
|
||||||
"connectedClient": "연결된 클라이언트",
|
"connectedClient": "연결된 클라이언트",
|
||||||
"resourceBlocked": "리소스 차단됨",
|
"resourceBlocked": "리소스 차단됨",
|
||||||
"droppedByRule": "룰에 의해 드롭됨",
|
"droppedByRule": "룰에 의해 드롭됨",
|
||||||
@@ -2660,19 +2678,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": "접근 로그를 얼마나 오래 보관할지",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "데이터독",
|
"streamingDatadogTitle": "데이터독",
|
||||||
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
|
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
|
||||||
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
|
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
|
||||||
"streamingFailedToLoad": "대상 로드에 실패했습니다",
|
"streamingLastSyncError": "마지막 동기화에서 오류가 발생했습니다.",
|
||||||
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
|
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
|
||||||
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
|
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
|
||||||
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
|
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "대상지 수정",
|
"S3DestEditTitle": "대상지 수정",
|
||||||
"S3DestAddTitle": "S3 대상지 추가",
|
"S3DestAddTitle": "S3 대상지 추가",
|
||||||
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||||
"S3DestAddDescription": "조직의 이벤트를 받기 위한 새로운 S3 엔드포인트를 구성하세요.",
|
"S3DestAddDescription": "조직의 이벤트를 수신할 새로운 Amazon S3(또는 S3 호환) 버킷을 구성하세요.",
|
||||||
|
"s3DestTabSettings": "설정",
|
||||||
|
"s3DestTabFormat": "형식",
|
||||||
|
"s3DestNameLabel": "이름",
|
||||||
|
"s3DestNamePlaceholder": "내 S3 대상",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS 액세스 키 ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS 비밀 액세스 키",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "귀하의 AWS 비밀 액세스 키",
|
||||||
|
"s3DestRegionLabel": "AWS 지역",
|
||||||
|
"s3DestBucketLabel": "버킷 이름",
|
||||||
|
"s3DestPrefixLabel": "키 접두사(선택 사항)",
|
||||||
|
"s3DestPrefixDescription": "하나의 객체 키 앞에 붙이는 선택적 경로 접두사입니다. 객체는 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}에 저장됩니다.",
|
||||||
|
"s3DestEndpointLabel": "사용자 정의 엔드포인트(선택 사항)",
|
||||||
|
"s3DestEndpointDescription": "MinIO 또는 Cloudflare R2와 같은 S3 호환 저장소에 대한 S3 엔드포인트를 재정의합니다. 표준 AWS S3의 경우 비워 두십시오.",
|
||||||
|
"s3DestGzipLabel": "Gzip 압축",
|
||||||
|
"s3DestGzipDescription": "각 업로드된 객체를 gzip으로 압축합니다. 저장 비용과 업로드 크기를 줄입니다.",
|
||||||
|
"s3DestFormatTitle": "파일 형식",
|
||||||
|
"s3DestFormatDescription": "업로드된 각 객체 내에서 이벤트가 직렬화되는 방식입니다.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "각 객체는 이벤트 기록의 JSON 배열입니다. 대부분의 분석 도구와 호환됩니다.",
|
||||||
|
"s3DestFormatNdjsonDescription": "각 객체는 한 줄당 하나의 JSON 레코드를 포함합니다(새 줄로 구분된 JSON). Athena, BigQuery, Spark와 호환됩니다.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "각 객체는 헤더 행이 있는 RFC-4180 CSV 파일입니다. 열 이름은 이벤트 데이터 필드에서 파생됩니다.",
|
||||||
|
"s3DestSaveChanges": "변경 사항 저장",
|
||||||
|
"s3DestCreateDestination": "대상 생성",
|
||||||
|
"s3DestUpdatedSuccess": "대상이 성공적으로 업데이트되었습니다",
|
||||||
|
"s3DestCreatedSuccess": "대상이 성공적으로 생성되었습니다",
|
||||||
|
"s3DestUpdateFailed": "대상 업데이트에 실패했습니다",
|
||||||
|
"s3DestCreateFailed": "대상 생성에 실패했습니다",
|
||||||
"datadogDestEditTitle": "대상지 수정",
|
"datadogDestEditTitle": "대상지 수정",
|
||||||
"datadogDestAddTitle": "Datadog 대상지 추가",
|
"datadogDestAddTitle": "Datadog 대상지 추가",
|
||||||
"datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
"datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||||
@@ -3134,7 +3179,7 @@
|
|||||||
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
||||||
"httpDestConnectionLogsTitle": "연결 로그",
|
"httpDestConnectionLogsTitle": "연결 로그",
|
||||||
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
||||||
"httpDestRequestLogsTitle": "요청 로그",
|
"httpDestRequestLogsTitle": "HTTP 요청 로그",
|
||||||
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
||||||
"httpDestSaveChanges": "변경 사항 저장",
|
"httpDestSaveChanges": "변경 사항 저장",
|
||||||
"httpDestCreateDestination": "대상지 생성",
|
"httpDestCreateDestination": "대상지 생성",
|
||||||
@@ -3208,5 +3253,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": "다음"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke",
|
"shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke",
|
||||||
"shareDeleted": "Lenke slettet",
|
"shareDeleted": "Lenke slettet",
|
||||||
"shareDeletedDescription": "Lenken har blitt slettet",
|
"shareDeletedDescription": "Lenken har blitt slettet",
|
||||||
|
"shareDelete": "Slett delingslenke",
|
||||||
|
"shareDeleteConfirm": "Bekreft sletting av delingslenke",
|
||||||
|
"shareQuestionRemove": "Er du sikker på at du vil slette denne delingslenken?",
|
||||||
|
"shareMessageRemove": "Når slettet, vil lenken ikke lenger fungere, og alle som bruker den vil miste tilgang til ressursen.",
|
||||||
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
|
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
|
||||||
"accessToken": "Tilgangsnøkkel",
|
"accessToken": "Tilgangsnøkkel",
|
||||||
"usageExamples": "Brukseksempler",
|
"usageExamples": "Brukseksempler",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.",
|
"userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.",
|
||||||
"userRemoveOrgConfirm": "Bekreft fjerning av bruker",
|
"userRemoveOrgConfirm": "Bekreft fjerning av bruker",
|
||||||
"userRemoveOrg": "Fjern bruker fra organisasjon",
|
"userRemoveOrg": "Fjern bruker fra organisasjon",
|
||||||
|
"userQuestionOrgRemoveSelf": "Er du sikker på at du vil fjerne deg selv fra denne organisasjonen?",
|
||||||
|
"userMessageOrgRemoveSelf": "Du vil miste tilgang umiddelbart. En administrator kan invitere deg igjen senere, men du må godta en ny invitasjon.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Bekreft fjerning av meg selv",
|
||||||
|
"userRemoveOrgSelf": "Fjern deg selv fra organisasjonen",
|
||||||
|
"userRemoveOrgSelfWarning": "Du vil miste tilgangen til denne organisasjonen umiddelbart.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "FJERN MEG SELV FRA ORG",
|
||||||
"users": "Brukere",
|
"users": "Brukere",
|
||||||
"accessRoleMember": "Medlem",
|
"accessRoleMember": "Medlem",
|
||||||
"accessRoleOwner": "Eier",
|
"accessRoleOwner": "Eier",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ugyldig e-postadresse",
|
"emailInvalid": "Ugyldig e-postadresse",
|
||||||
"inviteValidityDuration": "Vennligst velg en varighet",
|
"inviteValidityDuration": "Vennligst velg en varighet",
|
||||||
"accessRoleSelectPlease": "Vennligst velg en rolle",
|
"accessRoleSelectPlease": "Vennligst velg en rolle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Fjern din administratoradgang?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Du vil ikke lenger ha administratorrettigheter i denne organisasjonen etter lagring. En annen administrator kan gjenopprette tilgang hvis nødvendig.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Fjern min administratoradgang",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "FJERN MIN ADMINISTRATORADGANG",
|
||||||
|
"ownerMustRetainAdminRole": "Organisasjonseier må beholde minst én administratorrolle.",
|
||||||
"usernameRequired": "Brukernavn er påkrevd",
|
"usernameRequired": "Brukernavn er påkrevd",
|
||||||
"idpSelectPlease": "Vennligst velg en identitetsleverandør",
|
"idpSelectPlease": "Vennligst velg en identitetsleverandør",
|
||||||
"idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.",
|
"idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
||||||
"targetsSubmit": "Lagre mål",
|
"targetsSubmit": "Lagre mål",
|
||||||
"addTarget": "Legg til mål",
|
"addTarget": "Legg til mål",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Rundkjøringrutefordeling vil ikke fungere mellom steder som ikke er koblet til samme node, men failover vil fungere.",
|
||||||
"targetErrorInvalidIp": "Ugyldig IP-adresse",
|
"targetErrorInvalidIp": "Ugyldig IP-adresse",
|
||||||
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
|
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
|
||||||
"targetErrorInvalidPort": "Ugyldig port",
|
"targetErrorInvalidPort": "Ugyldig port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Gyldig passord",
|
"validPassword": "Gyldig passord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Vis",
|
||||||
|
"configManaged": "Konfigurasjon administrert",
|
||||||
"connectedClient": "Tilkoblet klient",
|
"connectedClient": "Tilkoblet klient",
|
||||||
"resourceBlocked": "Ressurs blokkert",
|
"resourceBlocked": "Ressurs blokkert",
|
||||||
"droppedByRule": "Legg i regelen",
|
"droppedByRule": "Legg i regelen",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
|
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
|
||||||
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
|
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
|
||||||
"streamingFailedToLoad": "Kan ikke laste inn destinasjoner",
|
"streamingLastSyncError": "Det oppstod en feil under siste synkronisering",
|
||||||
"streamingUnexpectedError": "En uventet feil oppstod.",
|
"streamingUnexpectedError": "En uventet feil oppstod.",
|
||||||
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
|
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
|
||||||
"streamingDeletedSuccess": "Målet ble slettet",
|
"streamingDeletedSuccess": "Målet ble slettet",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Rediger destinasjon",
|
"S3DestEditTitle": "Rediger destinasjon",
|
||||||
"S3DestAddTitle": "Legg til S3 destinasjon",
|
"S3DestAddTitle": "Legg til S3 destinasjon",
|
||||||
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
|
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
|
||||||
"S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens hendelser.",
|
"S3DestAddDescription": "Konfigurer en ny Amazon S3 (eller S3-kompatibel) bucket for å motta din organisasjons hendelser.",
|
||||||
|
"s3DestTabSettings": "Innstillinger",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Navn",
|
||||||
|
"s3DestNamePlaceholder": "Min S3-destinasjon",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS tilgangsnøkkel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS hemmelige tilgangsnøkkel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Din AWS secret access key",
|
||||||
|
"s3DestRegionLabel": "AWS-region",
|
||||||
|
"s3DestBucketLabel": "Bucket-navn",
|
||||||
|
"s3DestPrefixLabel": "Nøkkelprefiks (valgfritt)",
|
||||||
|
"s3DestPrefixDescription": "Valgfritt bane-prefiks lagt til hver objektnøkkel. Objekter er lagret på {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Egendefinert endepunkt (valgfritt)",
|
||||||
|
"s3DestEndpointDescription": "Overstyr S3-endepunktet for S3-kompatibel lagring som MinIO eller Cloudflare R2. La stå tomt for standard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-komprimering",
|
||||||
|
"s3DestGzipDescription": "Komprimer hvert opplastede objekt med gzip. Reduserer lagringskostnader og opplastingsstørrelse.",
|
||||||
|
"s3DestFormatTitle": "Filformat",
|
||||||
|
"s3DestFormatDescription": "Hvordan hendelser er serialisert inni hvert opplastede objekt.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Hvert objekt er et JSON-array av hendelsesposter. Kompatibel med de fleste analyseverktøy.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Hvert objekt inneholder en JSON-post per linje (nylinje-delt JSON). Kompatibel med Athena, BigQuery, og Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Hvert objekt er en RFC-4180 CSV-fil med en overskriftsrad. Kolonnenavn er avledet fra hendelsesdatafeltene.",
|
||||||
|
"s3DestSaveChanges": "Lagre endringer",
|
||||||
|
"s3DestCreateDestination": "Opprett destinasjon",
|
||||||
|
"s3DestUpdatedSuccess": "Destinasjon oppdatert vellykket",
|
||||||
|
"s3DestCreatedSuccess": "Destinasjon opprettet vellykket",
|
||||||
|
"s3DestUpdateFailed": "Kunne ikke oppdatere destinasjon",
|
||||||
|
"s3DestCreateFailed": "Kunne ikke opprette destinasjon",
|
||||||
"datadogDestEditTitle": "Rediger destinasjon",
|
"datadogDestEditTitle": "Rediger destinasjon",
|
||||||
"datadogDestAddTitle": "Legg til Datadog destinasjon",
|
"datadogDestAddTitle": "Legg til Datadog destinasjon",
|
||||||
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
|
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
|
||||||
@@ -3134,7 +3179,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",
|
||||||
@@ -3174,7 +3219,7 @@
|
|||||||
"publicIpEndpoint": "Endepunkt",
|
"publicIpEndpoint": "Endepunkt",
|
||||||
"lastTriggeredAt": "Siste utløste",
|
"lastTriggeredAt": "Siste utløste",
|
||||||
"reject": "Avvis",
|
"reject": "Avvis",
|
||||||
"uptimeDaysAgo": "{count} days ago",
|
"uptimeDaysAgo": "{count} dager siden",
|
||||||
"uptimeToday": "I dag",
|
"uptimeToday": "I dag",
|
||||||
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
|
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
|
||||||
"uptimeSuffix": "oppetid",
|
"uptimeSuffix": "oppetid",
|
||||||
@@ -3208,5 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link",
|
"shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link",
|
||||||
"shareDeleted": "Link verwijderd",
|
"shareDeleted": "Link verwijderd",
|
||||||
"shareDeletedDescription": "De link is verwijderd",
|
"shareDeletedDescription": "De link is verwijderd",
|
||||||
|
"shareDelete": "Verwijder Deel Link",
|
||||||
|
"shareDeleteConfirm": "Bevestig verwijdering van Deel Link",
|
||||||
|
"shareQuestionRemove": "Weet u zeker dat u deze deel link wilt verwijderen?",
|
||||||
|
"shareMessageRemove": "Zodra verwijderd, zal de link niet meer werken en zal iedereen die het gebruikt de toegang tot de bron verliezen.",
|
||||||
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.",
|
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.",
|
||||||
"accessToken": "Toegangs-token",
|
"accessToken": "Toegangs-token",
|
||||||
"usageExamples": "Voorbeelden van gebruik",
|
"usageExamples": "Voorbeelden van gebruik",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.",
|
"userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.",
|
||||||
"userRemoveOrgConfirm": "Bevestig verwijderen gebruiker",
|
"userRemoveOrgConfirm": "Bevestig verwijderen gebruiker",
|
||||||
"userRemoveOrg": "Gebruiker uit organisatie verwijderen",
|
"userRemoveOrg": "Gebruiker uit organisatie verwijderen",
|
||||||
|
"userQuestionOrgRemoveSelf": "Weet u zeker dat u zichzelf uit deze organisatie wilt verwijderen?",
|
||||||
|
"userMessageOrgRemoveSelf": "U verliest onmiddellijk toegang. Een beheerder kan u later opnieuw uitnodigen, maar u moet een nieuwe uitnodiging accepteren.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Bevestig Verwijder Mijn Persoon",
|
||||||
|
"userRemoveOrgSelf": "Verwijder uzelf uit de organisatie",
|
||||||
|
"userRemoveOrgSelfWarning": "U verliest onmiddellijk toegang tot deze organisatie.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "VERWIJDER MIJ UIT ORGANISATIE",
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"accessRoleMember": "Lid",
|
"accessRoleMember": "Lid",
|
||||||
"accessRoleOwner": "Eigenaar",
|
"accessRoleOwner": "Eigenaar",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ongeldig e-mailadres",
|
"emailInvalid": "Ongeldig e-mailadres",
|
||||||
"inviteValidityDuration": "Selecteer een tijdsduur",
|
"inviteValidityDuration": "Selecteer een tijdsduur",
|
||||||
"accessRoleSelectPlease": "Selecteer een rol",
|
"accessRoleSelectPlease": "Selecteer een rol",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Uw beheerderstoegang verwijderen?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "U zult na het opslaan geen beheerdersrechten meer hebben in deze organisatie. Een andere beheerder kan de toegang indien nodig herstellen.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Verwijder Mijn Beheerderstoegang",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "VERWIJDER MIJN BEHEERDERSTOEGANG",
|
||||||
|
"ownerMustRetainAdminRole": "De organisatie-eigenaar moet minstens één beheerdersrol behouden.",
|
||||||
"usernameRequired": "Gebruikersnaam is verplicht",
|
"usernameRequired": "Gebruikersnaam is verplicht",
|
||||||
"idpSelectPlease": "Selecteer een identiteitsprovider",
|
"idpSelectPlease": "Selecteer een identiteitsprovider",
|
||||||
"idpGenericOidc": "Algemene OAuth2/OIDC provider.",
|
"idpGenericOidc": "Algemene OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
||||||
"targetsSubmit": "Doelstellingen opslaan",
|
"targetsSubmit": "Doelstellingen opslaan",
|
||||||
"addTarget": "Doelwit toevoegen",
|
"addTarget": "Doelwit toevoegen",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round-robin routering werkt niet tussen locaties die niet met hetzelfde knooppunt zijn verbonden, maar failover werkt wel.",
|
||||||
"targetErrorInvalidIp": "Ongeldig IP-adres",
|
"targetErrorInvalidIp": "Ongeldig IP-adres",
|
||||||
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
|
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
|
||||||
"targetErrorInvalidPort": "Ongeldige poort",
|
"targetErrorInvalidPort": "Ongeldige poort",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Geldig wachtwoord",
|
"validPassword": "Geldig wachtwoord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Bekijk",
|
||||||
|
"configManaged": "Configuratie Beheerd",
|
||||||
"connectedClient": "Verbonden Client",
|
"connectedClient": "Verbonden Client",
|
||||||
"resourceBlocked": "Bron geblokkeerd",
|
"resourceBlocked": "Bron geblokkeerd",
|
||||||
"droppedByRule": "Achtergelaten door regel",
|
"droppedByRule": "Achtergelaten door regel",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
|
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
|
||||||
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
|
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
|
||||||
"streamingFailedToLoad": "Laden van bestemmingen mislukt",
|
"streamingLastSyncError": "Er is een fout opgetreden bij de laatste synchronisatie",
|
||||||
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
|
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
|
||||||
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
|
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
|
||||||
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
|
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Bestemming bewerken",
|
"S3DestEditTitle": "Bestemming bewerken",
|
||||||
"S3DestAddTitle": "S3-bestemming toevoegen",
|
"S3DestAddTitle": "S3-bestemming toevoegen",
|
||||||
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
|
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
|
||||||
"S3DestAddDescription": "Configureer een nieuw S3-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
|
"S3DestAddDescription": "Configureer een nieuwe Amazon S3 (of S3-compatibele) bucket om de gebeurtenissen van uw organisatie te ontvangen.",
|
||||||
|
"s3DestTabSettings": "Instellingen",
|
||||||
|
"s3DestTabFormat": "Formaat",
|
||||||
|
"s3DestNameLabel": "Naam",
|
||||||
|
"s3DestNamePlaceholder": "Mijn S3-bestemming",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS-toegangssleutel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Geheime Toegangssleutel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Uw AWS geheime toegangssleutel",
|
||||||
|
"s3DestRegionLabel": "AWS-regio",
|
||||||
|
"s3DestBucketLabel": "Bucketnaam",
|
||||||
|
"s3DestPrefixLabel": "Sleutelvoorvoegsel (optioneel)",
|
||||||
|
"s3DestPrefixDescription": "Optioneel padvoorvoegsel dat aan elke object sleutel wordt toegevoegd. Objecten worden opgeslagen op {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Aangepast Eindpunt (optioneel)",
|
||||||
|
"s3DestEndpointDescription": "Overschrijf het S3-eindpunt voor S3-compatibele opslag zoals MinIO of Cloudflare R2. Laat leeg voor standaard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-compressie",
|
||||||
|
"s3DestGzipDescription": "Comprimeer elk geüpload object met gzip. Verlaagt opslagkosten en uploadgrootte.",
|
||||||
|
"s3DestFormatTitle": "Bestandsformaat",
|
||||||
|
"s3DestFormatDescription": "Hoe gebeurtenissen binnen elk geüpload object worden geserialiseerd.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Elk object is een JSON-array van gebeurtenisrecords. Compatibel met de meeste analysetools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Elk object bevat één JSON-record per regel (nieuwregel-gescheiden JSON). Compatibel met Athena, BigQuery en Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Elk object is een RFC-4180 CSV-bestand met een kopregel. Kolomnamen zijn afgeleid van de gebeurtenis gegevensvelden.",
|
||||||
|
"s3DestSaveChanges": "Wijzigingen opslaan",
|
||||||
|
"s3DestCreateDestination": "Bestemming maken",
|
||||||
|
"s3DestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
|
||||||
|
"s3DestCreatedSuccess": "Bestemming succesvol gecreëerd",
|
||||||
|
"s3DestUpdateFailed": "Bijwerken bestemming mislukt",
|
||||||
|
"s3DestCreateFailed": "Aanmaken bestemming mislukt",
|
||||||
"datadogDestEditTitle": "Bestemming bewerken",
|
"datadogDestEditTitle": "Bestemming bewerken",
|
||||||
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
|
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
|
||||||
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
|
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku",
|
"shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku",
|
||||||
"shareDeleted": "Link usunięty",
|
"shareDeleted": "Link usunięty",
|
||||||
"shareDeletedDescription": "Link został usunięty",
|
"shareDeletedDescription": "Link został usunięty",
|
||||||
|
"shareDelete": "Usuń link udostępniania",
|
||||||
|
"shareDeleteConfirm": "Potwierdź usunięcie linku udostępniania",
|
||||||
|
"shareQuestionRemove": "Czy na pewno chcesz usunąć ten link udostępniania?",
|
||||||
|
"shareMessageRemove": "Po usunięciu, link przestanie działać i wszyscy korzystający z niego stracą dostęp do zasobu.",
|
||||||
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.",
|
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.",
|
||||||
"accessToken": "Token dostępu",
|
"accessToken": "Token dostępu",
|
||||||
"usageExamples": "Przykłady użycia",
|
"usageExamples": "Przykłady użycia",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.",
|
"userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.",
|
||||||
"userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika",
|
"userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika",
|
||||||
"userRemoveOrg": "Usuń użytkownika z organizacji",
|
"userRemoveOrg": "Usuń użytkownika z organizacji",
|
||||||
|
"userQuestionOrgRemoveSelf": "Czy na pewno chcesz usunąć się z tej organizacji?",
|
||||||
|
"userMessageOrgRemoveSelf": "Stracisz dostęp natychmiastowo. Administrator może cię ponownie zaprosić, ale będziesz musiał przyjąć nowe zaproszenie.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Potwierdź usunięcie siebie",
|
||||||
|
"userRemoveOrgSelf": "Usuń siebie z organizacji",
|
||||||
|
"userRemoveOrgSelfWarning": "Natychmiast stracisz dostęp do tej organizacji.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "USUŃ SIEBIE Z ORGANIZACJI",
|
||||||
"users": "Użytkownicy",
|
"users": "Użytkownicy",
|
||||||
"accessRoleMember": "Członek",
|
"accessRoleMember": "Członek",
|
||||||
"accessRoleOwner": "Właściciel",
|
"accessRoleOwner": "Właściciel",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Nieprawidłowy adres e-mail",
|
"emailInvalid": "Nieprawidłowy adres e-mail",
|
||||||
"inviteValidityDuration": "Proszę wybrać okres ważności",
|
"inviteValidityDuration": "Proszę wybrać okres ważności",
|
||||||
"accessRoleSelectPlease": "Proszę wybrać rolę",
|
"accessRoleSelectPlease": "Proszę wybrać rolę",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Usunąć dostęp administratora?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Po zapisaniu nie będziesz już posiadał uprawnień administratora w tej organizacji. Inny administrator może przywrócić dostęp, jeśli to konieczne.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Usuń mój dostęp administratora",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "USUŃ MÓJ DOSTĘP ADMINISTRATORA",
|
||||||
|
"ownerMustRetainAdminRole": "Właściciel organizacji musi zachować co najmniej jedną rolę administratora.",
|
||||||
"usernameRequired": "Nazwa użytkownika jest wymagana",
|
"usernameRequired": "Nazwa użytkownika jest wymagana",
|
||||||
"idpSelectPlease": "Proszę wybrać dostawcę tożsamości",
|
"idpSelectPlease": "Proszę wybrać dostawcę tożsamości",
|
||||||
"idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.",
|
"idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
||||||
"targetsSubmit": "Zapisz cele",
|
"targetsSubmit": "Zapisz cele",
|
||||||
"addTarget": "Dodaj cel",
|
"addTarget": "Dodaj cel",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Trasowanie round-robin nie będzie działać między witrynami, które nie są połączone z tym samym węzłem, ale przełączanie awaryjne będzie działać.",
|
||||||
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
|
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
|
||||||
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
|
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
|
||||||
"targetErrorInvalidPort": "Nieprawidłowy port",
|
"targetErrorInvalidPort": "Nieprawidłowy port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Prawidłowe hasło",
|
"validPassword": "Prawidłowe hasło",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Zobacz",
|
||||||
|
"configManaged": "Konfiguracja zarządzana",
|
||||||
"connectedClient": "Połączony Klient",
|
"connectedClient": "Połączony Klient",
|
||||||
"resourceBlocked": "Zasób zablokowany",
|
"resourceBlocked": "Zasób zablokowany",
|
||||||
"droppedByRule": "Upuszczone przez regułę",
|
"droppedByRule": "Upuszczone przez regułę",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.",
|
"streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.",
|
||||||
"streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.",
|
"streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.",
|
||||||
"streamingFailedToLoad": "Nie udało się załadować miejsc docelowych",
|
"streamingLastSyncError": "Wystąpił błąd podczas ostatniej synchronizacji",
|
||||||
"streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.",
|
"streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.",
|
||||||
"streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego",
|
"streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego",
|
||||||
"streamingDeletedSuccess": "Cel usunięty pomyślnie",
|
"streamingDeletedSuccess": "Cel usunięty pomyślnie",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Edytuj Miejsce Docelowe",
|
"S3DestEditTitle": "Edytuj Miejsce Docelowe",
|
||||||
"S3DestAddTitle": "Dodaj Miejsce Docelowe S3",
|
"S3DestAddTitle": "Dodaj Miejsce Docelowe S3",
|
||||||
"S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.",
|
"S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.",
|
||||||
"S3DestAddDescription": "Skonfiguruj nowy punkt końcowy S3, aby odbierać zdarzenia Twojej organizacji.",
|
"S3DestAddDescription": "Skonfiguruj nowy zasobnik Amazon S3 (lub zgodny z S3), aby otrzymywać zdarzenia twojej organizacji.",
|
||||||
|
"s3DestTabSettings": "Ustawienia",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Nazwa",
|
||||||
|
"s3DestNamePlaceholder": "Moje miejsce docelowe S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Twój AWS Secret Access Key",
|
||||||
|
"s3DestRegionLabel": "Region AWS",
|
||||||
|
"s3DestBucketLabel": "Nazwa kubła",
|
||||||
|
"s3DestPrefixLabel": "Prefiks klucza (opcjonalnie)",
|
||||||
|
"s3DestPrefixDescription": "Opcjonalny prefiks ścieżki dołączony do każdego klucza obiektu. Obiekty są przechowywane w {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Niestandardowy punkt końcowy (opcjonalnie)",
|
||||||
|
"s3DestEndpointDescription": "Nadpisz punkt końcowy S3 dla zgodnego przechowywania danych, takiego jak MinIO lub Cloudflare R2. Pozostaw puste dla standardowego AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Kompresja Gzip",
|
||||||
|
"s3DestGzipDescription": "Skompresuj każdy przesłany obiekt za pomocą gzip. Zmniejsza koszty przechowywania i rozmiar przesyłu.",
|
||||||
|
"s3DestFormatTitle": "Format pliku",
|
||||||
|
"s3DestFormatDescription": "Jak zdarzenia są serializowane w każdym przesłanym obiekcie.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Każdy obiekt to tablica JSON z rekordami zdarzeń. Zgodne z większością narzędzi analitycznych.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Każdy obiekt zawiera jeden rekord JSON na linię (nowa linia-dzielone JSON). Zgodne z Athena, BigQuery i Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Każdy obiekt to plik CSV zgodny z RFC-4180 z wierszem nagłówka. Nazwy kolumn pochodzą z pól danych zdarzeń.",
|
||||||
|
"s3DestSaveChanges": "Zapisz zmiany",
|
||||||
|
"s3DestCreateDestination": "Utwórz miejsce docelowe",
|
||||||
|
"s3DestUpdatedSuccess": "Miejsce docelowe zaktualizowane pomyślnie",
|
||||||
|
"s3DestCreatedSuccess": "Miejsce docelowe utworzone pomyślnie",
|
||||||
|
"s3DestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego",
|
||||||
|
"s3DestCreateFailed": "Nie udało się utworzyć miejsca docelowego",
|
||||||
"datadogDestEditTitle": "Edytuj Miejsce Docelowe",
|
"datadogDestEditTitle": "Edytuj Miejsce Docelowe",
|
||||||
"datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog",
|
"datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog",
|
||||||
"datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.",
|
"datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link",
|
"shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link",
|
||||||
"shareDeleted": "Link excluído",
|
"shareDeleted": "Link excluído",
|
||||||
"shareDeletedDescription": "O link foi eliminado",
|
"shareDeletedDescription": "O link foi eliminado",
|
||||||
|
"shareDelete": "Excluir Link de Compartilhamento",
|
||||||
|
"shareDeleteConfirm": "Confirmar Exclusão de Link de Compartilhamento",
|
||||||
|
"shareQuestionRemove": "Tem certeza de que deseja excluir este link de compartilhamento?",
|
||||||
|
"shareMessageRemove": "Uma vez excluído, o link não funcionará mais e qualquer pessoa que o utilizar perderá o acesso ao recurso.",
|
||||||
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.",
|
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.",
|
||||||
"accessToken": "Token de acesso",
|
"accessToken": "Token de acesso",
|
||||||
"usageExamples": "Exemplos de uso",
|
"usageExamples": "Exemplos de uso",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.",
|
"userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.",
|
||||||
"userRemoveOrgConfirm": "Confirmar Remoção do Usuário",
|
"userRemoveOrgConfirm": "Confirmar Remoção do Usuário",
|
||||||
"userRemoveOrg": "Remover Usuário da Organização",
|
"userRemoveOrg": "Remover Usuário da Organização",
|
||||||
|
"userQuestionOrgRemoveSelf": "Tem certeza de que deseja se remover desta organização?",
|
||||||
|
"userMessageOrgRemoveSelf": "Você perderá o acesso imediatamente. Um administrador poderá convidá-lo novamente mais tarde, mas você precisará aceitar um novo convite.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmar a Remoção de Mim Mesmo",
|
||||||
|
"userRemoveOrgSelf": "Remover-se da organização",
|
||||||
|
"userRemoveOrgSelfWarning": "Você perderá o acesso a esta organização imediatamente.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "REMOVER-ME DA ORG",
|
||||||
"users": "Utilizadores",
|
"users": "Utilizadores",
|
||||||
"accessRoleMember": "Membro",
|
"accessRoleMember": "Membro",
|
||||||
"accessRoleOwner": "Proprietário",
|
"accessRoleOwner": "Proprietário",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Endereço de email inválido",
|
"emailInvalid": "Endereço de email inválido",
|
||||||
"inviteValidityDuration": "Por favor, selecione uma duração",
|
"inviteValidityDuration": "Por favor, selecione uma duração",
|
||||||
"accessRoleSelectPlease": "Por favor, selecione uma função",
|
"accessRoleSelectPlease": "Por favor, selecione uma função",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Remover seu acesso de administrador?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Você não terá mais permissões de administrador nesta organização após salvar. Outro administrador pode restaurar seu acesso, se necessário.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Remover Meu Acesso de Administrador",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "REMOVER MEU ACESSO DE ADMIN",
|
||||||
|
"ownerMustRetainAdminRole": "O proprietário da organização deve manter pelo menos um papel de administrador.",
|
||||||
"usernameRequired": "Nome de utilizador é obrigatório",
|
"usernameRequired": "Nome de utilizador é obrigatório",
|
||||||
"idpSelectPlease": "Por favor, selecione um provedor de identidade",
|
"idpSelectPlease": "Por favor, selecione um provedor de identidade",
|
||||||
"idpGenericOidc": "Provedor genérico OAuth2/OIDC.",
|
"idpGenericOidc": "Provedor genérico OAuth2/OIDC.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
||||||
"targetsSubmit": "Guardar Alvos",
|
"targetsSubmit": "Guardar Alvos",
|
||||||
"addTarget": "Adicionar Alvo",
|
"addTarget": "Adicionar Alvo",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "O roteamento round robin não funcionará entre sites que não estão conectados ao mesmo nó, mas o failover funcionará.",
|
||||||
"targetErrorInvalidIp": "Endereço IP inválido",
|
"targetErrorInvalidIp": "Endereço IP inválido",
|
||||||
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
||||||
"targetErrorInvalidPort": "Porta inválida",
|
"targetErrorInvalidPort": "Porta inválida",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Senha válida",
|
"validPassword": "Senha válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Visualizar",
|
||||||
|
"configManaged": "Configuração Gerenciada",
|
||||||
"connectedClient": "Cliente Conectado",
|
"connectedClient": "Cliente Conectado",
|
||||||
"resourceBlocked": "Recurso bloqueado",
|
"resourceBlocked": "Recurso bloqueado",
|
||||||
"droppedByRule": "Derrubado pela regra",
|
"droppedByRule": "Derrubado pela regra",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.",
|
"streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.",
|
||||||
"streamingTypePickerDescription": "Escolha um tipo de destino para começar.",
|
"streamingTypePickerDescription": "Escolha um tipo de destino para começar.",
|
||||||
"streamingFailedToLoad": "Falha ao carregar destinos",
|
"streamingLastSyncError": "Ocorreu um erro na última sincronização",
|
||||||
"streamingUnexpectedError": "Ocorreu um erro inesperado.",
|
"streamingUnexpectedError": "Ocorreu um erro inesperado.",
|
||||||
"streamingFailedToUpdate": "Falha ao atualizar destino",
|
"streamingFailedToUpdate": "Falha ao atualizar destino",
|
||||||
"streamingDeletedSuccess": "Destino apagado com sucesso",
|
"streamingDeletedSuccess": "Destino apagado com sucesso",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Editar Destino",
|
"S3DestEditTitle": "Editar Destino",
|
||||||
"S3DestAddTitle": "Adicionar Destino S3",
|
"S3DestAddTitle": "Adicionar Destino S3",
|
||||||
"S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.",
|
"S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.",
|
||||||
"S3DestAddDescription": "Configure um novo endpoint S3 para receber os eventos da sua organização.",
|
"S3DestAddDescription": "Configure um novo bucket Amazon S3 (ou compatível com S3) para receber os eventos da sua organização.",
|
||||||
|
"s3DestTabSettings": "Configurações",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nome",
|
||||||
|
"s3DestNamePlaceholder": "Meu destino S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID da Chave de Acesso AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Chave de Acesso Secreta AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Sua chave de acesso secreta AWS",
|
||||||
|
"s3DestRegionLabel": "Região AWS",
|
||||||
|
"s3DestBucketLabel": "Nome do Bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefixo da Chave (opcional)",
|
||||||
|
"s3DestPrefixDescription": "Prefixo de caminho opcional adicionado a cada chave de objeto. Os objetos são armazenados em {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Endpoint Personalizado (opcional)",
|
||||||
|
"s3DestEndpointDescription": "Substitua o endpoint S3 por armazenamento compatível com S3, como MinIO ou Cloudflare R2. Deixe em branco para o padrão AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Compressão Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprime cada objeto carregado com gzip. Reduz custos de armazenamento e tamanho de upload.",
|
||||||
|
"s3DestFormatTitle": "Formato de Arquivo",
|
||||||
|
"s3DestFormatDescription": "Como os eventos são serializados dentro de cada objeto carregado.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Cada objeto é um array JSON de registros de eventos. Compatível com a maioria das ferramentas de análise.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Cada objeto contém um registro JSON por linha (JSON delimitado por nova linha). Compatível com Athena, BigQuery e Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Cada objeto é um arquivo CSV RFC-4180 com uma linha de cabeçalho. Nomes de colunas são derivados dos campos de dados do evento.",
|
||||||
|
"s3DestSaveChanges": "Salvar Alterações",
|
||||||
|
"s3DestCreateDestination": "Criar Destino",
|
||||||
|
"s3DestUpdatedSuccess": "Destino atualizado com sucesso",
|
||||||
|
"s3DestCreatedSuccess": "Destino criado com sucesso",
|
||||||
|
"s3DestUpdateFailed": "Falha ao atualizar destino",
|
||||||
|
"s3DestCreateFailed": "Falha ao criar destino",
|
||||||
"datadogDestEditTitle": "Editar Destino",
|
"datadogDestEditTitle": "Editar Destino",
|
||||||
"datadogDestAddTitle": "Adicionar Destino Datadog",
|
"datadogDestAddTitle": "Adicionar Destino Datadog",
|
||||||
"datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.",
|
"datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки",
|
"shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки",
|
||||||
"shareDeleted": "Ссылка удалена",
|
"shareDeleted": "Ссылка удалена",
|
||||||
"shareDeletedDescription": "Ссылка была успешно удалена",
|
"shareDeletedDescription": "Ссылка была успешно удалена",
|
||||||
|
"shareDelete": "Удалить общую ссылку",
|
||||||
|
"shareDeleteConfirm": "Подтвердите удаление общей ссылки",
|
||||||
|
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
|
||||||
|
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
|
||||||
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
|
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
|
||||||
"accessToken": "Токен доступа",
|
"accessToken": "Токен доступа",
|
||||||
"usageExamples": "Примеры использования",
|
"usageExamples": "Примеры использования",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
|
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
|
||||||
"userRemoveOrgConfirm": "Подтвердить удаление пользователя",
|
"userRemoveOrgConfirm": "Подтвердить удаление пользователя",
|
||||||
"userRemoveOrg": "Удалить пользователя из организации",
|
"userRemoveOrg": "Удалить пользователя из организации",
|
||||||
|
"userQuestionOrgRemoveSelf": "Вы уверены, что хотите удалить себя из этой организации?",
|
||||||
|
"userMessageOrgRemoveSelf": "Вы немедленно потеряете доступ. Администратор сможет снова пригласить вас позже, но вам нужно будет принять новое приглашение.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Подтвердите удаление себя",
|
||||||
|
"userRemoveOrgSelf": "Удалите себя из организации",
|
||||||
|
"userRemoveOrgSelfWarning": "Вы немедленно потеряете доступ к этой организации.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "Удалить себя из организации",
|
||||||
"users": "Пользователи",
|
"users": "Пользователи",
|
||||||
"accessRoleMember": "Участник",
|
"accessRoleMember": "Участник",
|
||||||
"accessRoleOwner": "Владелец",
|
"accessRoleOwner": "Владелец",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Неверный адрес Email",
|
"emailInvalid": "Неверный адрес Email",
|
||||||
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
|
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
|
||||||
"accessRoleSelectPlease": "Пожалуйста, выберите роль",
|
"accessRoleSelectPlease": "Пожалуйста, выберите роль",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Удалить доступ администратора?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "После сохранения у вас больше не будет прав администратора в этой организации. Другой администратор может восстановить доступ, если это необходимо.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Удалить мой доступ администратора",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "УДАЛИТЬ МОЙ ДОСТУП АДМИНИСТРАТОРА",
|
||||||
|
"ownerMustRetainAdminRole": "Владелец организации должен сохранить по крайней мере одну роль администратора.",
|
||||||
"usernameRequired": "Имя пользователя обязательно",
|
"usernameRequired": "Имя пользователя обязательно",
|
||||||
"idpSelectPlease": "Пожалуйста, выберите Identity Provider",
|
"idpSelectPlease": "Пожалуйста, выберите Identity Provider",
|
||||||
"idpGenericOidc": "Обычный OAuth2/OIDC provider.",
|
"idpGenericOidc": "Обычный OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
||||||
"targetsSubmit": "Сохранить цели",
|
"targetsSubmit": "Сохранить цели",
|
||||||
"addTarget": "Добавить цель",
|
"addTarget": "Добавить цель",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Роутинг с балансировкой нагрузки не будет работать между сайтами, не подключенными к одному и тому же узлу, но подмена будет работать.",
|
||||||
"targetErrorInvalidIp": "Неверный IP-адрес",
|
"targetErrorInvalidIp": "Неверный IP-адрес",
|
||||||
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
||||||
"targetErrorInvalidPort": "Неверный порт",
|
"targetErrorInvalidPort": "Неверный порт",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Допустимый пароль",
|
"validPassword": "Допустимый пароль",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Просмотр",
|
||||||
|
"configManaged": "Конфигурация управляется",
|
||||||
"connectedClient": "Подключенный клиент",
|
"connectedClient": "Подключенный клиент",
|
||||||
"resourceBlocked": "Ресурс заблокирован",
|
"resourceBlocked": "Ресурс заблокирован",
|
||||||
"droppedByRule": "Отброшено по правилам",
|
"droppedByRule": "Отброшено по правилам",
|
||||||
@@ -2660,19 +2678,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": "Как долго сохранять журналы доступа",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
|
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
|
||||||
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
|
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
|
||||||
"streamingFailedToLoad": "Не удалось загрузить места назначения",
|
"streamingLastSyncError": "Во время последней синхронизации произошла ошибка",
|
||||||
"streamingUnexpectedError": "Произошла непредвиденная ошибка.",
|
"streamingUnexpectedError": "Произошла непредвиденная ошибка.",
|
||||||
"streamingFailedToUpdate": "Не удалось обновить место назначения",
|
"streamingFailedToUpdate": "Не удалось обновить место назначения",
|
||||||
"streamingDeletedSuccess": "Адрес назначения успешно удален",
|
"streamingDeletedSuccess": "Адрес назначения успешно удален",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Редактировать пункт назначения",
|
"S3DestEditTitle": "Редактировать пункт назначения",
|
||||||
"S3DestAddTitle": "Добавить S3 пункт назначения",
|
"S3DestAddTitle": "Добавить S3 пункт назначения",
|
||||||
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
|
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
|
||||||
"S3DestAddDescription": "Настройте новую S3 конечную точку для получения событий вашей организации.",
|
"S3DestAddDescription": "Настройте новый Amazon S3 (или совместимое S3) хранилище для получения событий вашей организации.",
|
||||||
|
"s3DestTabSettings": "Настройки",
|
||||||
|
"s3DestTabFormat": "Формат",
|
||||||
|
"s3DestNameLabel": "Имя",
|
||||||
|
"s3DestNamePlaceholder": "Моя S3 конечная точка",
|
||||||
|
"s3DestAccessKeyIdLabel": "Идентификатор ключа доступа AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Секретный ключ доступа AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Ваш секретный ключ доступа AWS",
|
||||||
|
"s3DestRegionLabel": "Регион AWS",
|
||||||
|
"s3DestBucketLabel": "Имя хранилища",
|
||||||
|
"s3DestPrefixLabel": "Префикс ключа (по желанию)",
|
||||||
|
"s3DestPrefixDescription": "Необязательный префикс пути, добавляется к каждому ключу объекта. Объекты хранятся в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Пользовательская конечная точка (по желанию)",
|
||||||
|
"s3DestEndpointDescription": "Переопределите конечную точку S3 для совместимого хранилища, такого как MinIO или Cloudflare R2. Оставьте пустым для стандартного AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Сжатие Gzip",
|
||||||
|
"s3DestGzipDescription": "Сжимайте каждый загруженный объект с помощью gzip. Уменьшает стоимость хранения и размер загрузки.",
|
||||||
|
"s3DestFormatTitle": "Формат файла",
|
||||||
|
"s3DestFormatDescription": "Как события сериализуются внутри каждого загруженного объекта.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Каждый объект — это JSON массив записей событий. Совместим с большинством аналитических инструментов.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Каждый объект содержит одну запись JSON на строку (JSON, разделённый новой строкой). Совместим с Athena, BigQuery и Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Каждый объект представляет собой CSV файл по стандарту RFC-4180 с заголовочной строкой. Имена столбцов выведены из полей данных событий.",
|
||||||
|
"s3DestSaveChanges": "Сохранить изменения",
|
||||||
|
"s3DestCreateDestination": "Создать конечную точку",
|
||||||
|
"s3DestUpdatedSuccess": "Конечная точка успешно обновлена",
|
||||||
|
"s3DestCreatedSuccess": "Конечная точка успешно создана",
|
||||||
|
"s3DestUpdateFailed": "Не удалось обновить конечную точку",
|
||||||
|
"s3DestCreateFailed": "Не удалось создать конечную точку",
|
||||||
"datadogDestEditTitle": "Редактировать пункт назначения",
|
"datadogDestEditTitle": "Редактировать пункт назначения",
|
||||||
"datadogDestAddTitle": "Добавить пункт назначения Datadog",
|
"datadogDestAddTitle": "Добавить пункт назначения Datadog",
|
||||||
"datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.",
|
"datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.",
|
||||||
@@ -3134,7 +3179,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
||||||
"httpDestConnectionLogsTitle": "Журнал подключений",
|
"httpDestConnectionLogsTitle": "Журнал подключений",
|
||||||
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
||||||
"httpDestRequestLogsTitle": "Запросить журналы",
|
"httpDestRequestLogsTitle": "HTTP Запросы Логи",
|
||||||
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
||||||
"httpDestSaveChanges": "Сохранить изменения",
|
"httpDestSaveChanges": "Сохранить изменения",
|
||||||
"httpDestCreateDestination": "Создать адрес назначения",
|
"httpDestCreateDestination": "Создать адрес назначения",
|
||||||
@@ -3208,5 +3253,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": "Следующий"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Bağlantı silinirken bir hata oluştu",
|
"shareErrorDeleteMessage": "Bağlantı silinirken bir hata oluştu",
|
||||||
"shareDeleted": "Bağlantı silindi",
|
"shareDeleted": "Bağlantı silindi",
|
||||||
"shareDeletedDescription": "Bağlantı silindi",
|
"shareDeletedDescription": "Bağlantı silindi",
|
||||||
|
"shareDelete": "Paylaşım Bağlantısını Sil",
|
||||||
|
"shareDeleteConfirm": "Paylaşım Bağlantısının Silinmesini Onayla",
|
||||||
|
"shareQuestionRemove": "Bu paylaşım bağlantısını silmek istediğinizden emin misiniz?",
|
||||||
|
"shareMessageRemove": "Silindikten sonra, bağlantı artık çalışmayacak ve kullanan herkes kaynağa erişimini kaybedecek.",
|
||||||
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
|
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
|
||||||
"accessToken": "Erişim Jetonu",
|
"accessToken": "Erişim Jetonu",
|
||||||
"usageExamples": "Kullanım Örnekleri",
|
"usageExamples": "Kullanım Örnekleri",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
||||||
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
||||||
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
||||||
|
"userQuestionOrgRemoveSelf": "Bu organizasyondan kendinizi kaldırmak istediğinizden emin misiniz?",
|
||||||
|
"userMessageOrgRemoveSelf": "Erişiminizi hemen kaybedeceksiniz. Bir yönetici daha sonra sizi tekrar davet edebilir, ancak yeni bir daveti kabul etmeniz gerekecek.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Kendimi Kaldırmayı Onayla",
|
||||||
|
"userRemoveOrgSelf": "Kendinizi organizasyondan kaldırın",
|
||||||
|
"userRemoveOrgSelfWarning": "Bu organizasyona erişiminizi anında kaybedeceksiniz.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "KENDİMİ ORGANİZASYONDAN KALDIR",
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
"accessRoleMember": "Üye",
|
"accessRoleMember": "Üye",
|
||||||
"accessRoleOwner": "Sahip",
|
"accessRoleOwner": "Sahip",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Geçersiz e-posta adresi",
|
"emailInvalid": "Geçersiz e-posta adresi",
|
||||||
"inviteValidityDuration": "Lütfen bir süre seçin",
|
"inviteValidityDuration": "Lütfen bir süre seçin",
|
||||||
"accessRoleSelectPlease": "Lütfen bir rol seçin",
|
"accessRoleSelectPlease": "Lütfen bir rol seçin",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Yönetici erişiminizi kaldırmak istiyor musunuz?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Kaydettikten sonra, bu organizasyonda artık yönetici izinleriniz olmayacak. Gerekirse başka bir yönetici erişimi geri yükleyebilir.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Yönetici Erişimi Kaldır",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "YÖNETİCİ ERİŞİMİMİ KALDIR",
|
||||||
|
"ownerMustRetainAdminRole": "Organizasyon sahibi en az bir yönetici rolü bulundurmalıdır.",
|
||||||
"usernameRequired": "Kullanıcı adı gereklidir",
|
"usernameRequired": "Kullanıcı adı gereklidir",
|
||||||
"idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin",
|
"idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin",
|
||||||
"idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.",
|
"idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
||||||
"targetsSubmit": "Hedefleri Kaydet",
|
"targetsSubmit": "Hedefleri Kaydet",
|
||||||
"addTarget": "Hedef Ekle",
|
"addTarget": "Hedef Ekle",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin yönlendirme, aynı düğüme bağlı olmayan siteler arasında çalışmayacaktır, ancak failover çalışacaktır.",
|
||||||
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
||||||
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
||||||
"targetErrorInvalidPort": "Geçersiz port",
|
"targetErrorInvalidPort": "Geçersiz port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Geçerli Şifre",
|
"validPassword": "Geçerli Şifre",
|
||||||
"validEmail": "Geçerli E-posta",
|
"validEmail": "Geçerli E-posta",
|
||||||
"validSSO": "Geçerli SSO",
|
"validSSO": "Geçerli SSO",
|
||||||
|
"view": "Görüntüle",
|
||||||
|
"configManaged": "Yapılandırma Yönetildi",
|
||||||
"connectedClient": "Bağlı İstemci",
|
"connectedClient": "Bağlı İstemci",
|
||||||
"resourceBlocked": "Kaynak Engellendi",
|
"resourceBlocked": "Kaynak Engellendi",
|
||||||
"droppedByRule": "Kurallara Göre Çıkartıldı",
|
"droppedByRule": "Kurallara Göre Çıkartıldı",
|
||||||
@@ -2660,19 +2678,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",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.",
|
"streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.",
|
||||||
"streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.",
|
"streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.",
|
||||||
"streamingFailedToLoad": "Hedefler yüklenemedi",
|
"streamingLastSyncError": "Son senkronizasyonda bir hata oluştu",
|
||||||
"streamingUnexpectedError": "Beklenmeyen bir hata oluştu.",
|
"streamingUnexpectedError": "Beklenmeyen bir hata oluştu.",
|
||||||
"streamingFailedToUpdate": "Hedef güncellenemedi",
|
"streamingFailedToUpdate": "Hedef güncellenemedi",
|
||||||
"streamingDeletedSuccess": "Hedef başarıyla silindi",
|
"streamingDeletedSuccess": "Hedef başarıyla silindi",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Hedefi Düzenle",
|
"S3DestEditTitle": "Hedefi Düzenle",
|
||||||
"S3DestAddTitle": "S3 Hedefi Ekle",
|
"S3DestAddTitle": "S3 Hedefi Ekle",
|
||||||
"S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.",
|
"S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||||
"S3DestAddDescription": "Kuruluşunuzun olaylarını almak için yeni bir S3 uç noktası yapılandırın.",
|
"S3DestAddDescription": "Kuruluşunuzun etkinliklerini almak için yeni bir Amazon S3 (veya S3-uyumlu) kovası yapılandırın.",
|
||||||
|
"s3DestTabSettings": "Ayarlar",
|
||||||
|
"s3DestTabFormat": "Biçim",
|
||||||
|
"s3DestNameLabel": "Ad",
|
||||||
|
"s3DestNamePlaceholder": "Benim S3 hedefim",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Erişim Anahtar Kimliği",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Gizli Erişim Anahtarı",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "AWS gizli erişim anahtarınız",
|
||||||
|
"s3DestRegionLabel": "AWS Bölgesi",
|
||||||
|
"s3DestBucketLabel": "Kova Adı",
|
||||||
|
"s3DestPrefixLabel": "Anahtar Ön Eki (isteğe bağlı)",
|
||||||
|
"s3DestPrefixDescription": "Her nesne anahtarının önüne eklenen isteğe bağlı yol öneki. Nesneler {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} konumunda saklanır.",
|
||||||
|
"s3DestEndpointLabel": "Özel Uç Nokta (isteğe bağlı)",
|
||||||
|
"s3DestEndpointDescription": "MinIO veya Cloudflare R2 gibi S3-uyumlu depolama için S3 uç noktasını geçersiz kılın. Standart AWS S3 için boş bırakın.",
|
||||||
|
"s3DestGzipLabel": "Gzip sıkıştırması",
|
||||||
|
"s3DestGzipDescription": "Her yüklü nesneyi gzip ile sıkıştırın. Depolama maliyetlerini ve yükleme boyutunu azaltır.",
|
||||||
|
"s3DestFormatTitle": "Dosya Biçimi",
|
||||||
|
"s3DestFormatDescription": "Etkinliklerin her yüklendiği nesne içinde nasıl serileştirildiği.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Her nesne bir olay kayıtlarının JSON dizisidir. Çoğu analiz aracıyla uyumludur.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Her nesne satır başına bir JSON kaydı içerir (yeni satır ile ayrılmış JSON). Athena, BigQuery ve Spark ile uyumludur.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Her nesne, bir başlık satırı ile birlikte RFC-4180 CSV dosyasıdır. Sütun isimleri olay verileri alanlarından türetilmiştir.",
|
||||||
|
"s3DestSaveChanges": "Değişiklikleri Kaydet",
|
||||||
|
"s3DestCreateDestination": "Hedef Oluştur",
|
||||||
|
"s3DestUpdatedSuccess": "Hedef başarıyla güncellendi",
|
||||||
|
"s3DestCreatedSuccess": "Hedef başarıyla oluşturuldu",
|
||||||
|
"s3DestUpdateFailed": "Hedef güncellenemedi",
|
||||||
|
"s3DestCreateFailed": "Hedef oluşturulamadı",
|
||||||
"datadogDestEditTitle": "Hedefi Düzenle",
|
"datadogDestEditTitle": "Hedefi Düzenle",
|
||||||
"datadogDestAddTitle": "Datadog Hedefi Ekle",
|
"datadogDestAddTitle": "Datadog Hedefi Ekle",
|
||||||
"datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.",
|
"datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||||
@@ -3134,7 +3179,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 +3253,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"trialActive": "免费试用中",
|
"trialActive": "免费试用中",
|
||||||
"trialExpired": "试用到期",
|
"trialExpired": "试用到期",
|
||||||
"trialHasEnded": "您的试用已结束。",
|
"trialHasEnded": "您的试用已结束。",
|
||||||
"trialDaysRemaining": "{count, plural, one {# day remaining} other {# days remaining}}",
|
"trialDaysRemaining": "{count, plural, other {# 天剩余}}",
|
||||||
"trialDaysLeftShort": "试用期剩余 {days} 天",
|
"trialDaysLeftShort": "试用期剩余 {days} 天",
|
||||||
"trialGoToBilling": "转到账单页面",
|
"trialGoToBilling": "转到账单页面",
|
||||||
"subscriptionViolationViewBilling": "查看计费",
|
"subscriptionViolationViewBilling": "查看计费",
|
||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "删除链接时出错",
|
"shareErrorDeleteMessage": "删除链接时出错",
|
||||||
"shareDeleted": "链接已删除",
|
"shareDeleted": "链接已删除",
|
||||||
"shareDeletedDescription": "链接已删除",
|
"shareDeletedDescription": "链接已删除",
|
||||||
|
"shareDelete": "删除共享链接",
|
||||||
|
"shareDeleteConfirm": "确认删除共享链接",
|
||||||
|
"shareQuestionRemove": "您确定要删除这个共享链接吗?",
|
||||||
|
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
|
||||||
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
|
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
|
||||||
"accessToken": "访问令牌",
|
"accessToken": "访问令牌",
|
||||||
"usageExamples": "用法示例",
|
"usageExamples": "用法示例",
|
||||||
@@ -303,7 +307,7 @@
|
|||||||
"accessUserManage": "管理用户",
|
"accessUserManage": "管理用户",
|
||||||
"accessUsersDescription": "邀请和管理访问此组织的用户",
|
"accessUsersDescription": "邀请和管理访问此组织的用户",
|
||||||
"accessUsersSearch": "搜索用户...",
|
"accessUsersSearch": "搜索用户...",
|
||||||
"accessUsersRoleFilterCount": "{count, plural, one {# role} other {# roles}}",
|
"accessUsersRoleFilterCount": "{count, plural, other {# 角色}}",
|
||||||
"accessUsersRoleFilterClear": "清除角色过滤器",
|
"accessUsersRoleFilterClear": "清除角色过滤器",
|
||||||
"accessUserCreate": "创建用户",
|
"accessUserCreate": "创建用户",
|
||||||
"accessUserRemove": "删除用户",
|
"accessUserRemove": "删除用户",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
|
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
|
||||||
"userRemoveOrgConfirm": "确认删除用户",
|
"userRemoveOrgConfirm": "确认删除用户",
|
||||||
"userRemoveOrg": "从组织中删除用户",
|
"userRemoveOrg": "从组织中删除用户",
|
||||||
|
"userQuestionOrgRemoveSelf": "你确定要将自己从这个组织中移除吗?",
|
||||||
|
"userMessageOrgRemoveSelf": "你将立即失去访问权限。管理员稍后可以再次邀请你,但你需要接受新的邀请。",
|
||||||
|
"userRemoveOrgConfirmSelf": "确认删除我自己",
|
||||||
|
"userRemoveOrgSelf": "将自己从组织中移除",
|
||||||
|
"userRemoveOrgSelfWarning": "你将立即失去对此组织的访问权限。",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "从组织中移除我自己",
|
||||||
"users": "用户",
|
"users": "用户",
|
||||||
"accessRoleMember": "成员",
|
"accessRoleMember": "成员",
|
||||||
"accessRoleOwner": "所有者",
|
"accessRoleOwner": "所有者",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "无效的电子邮件地址",
|
"emailInvalid": "无效的电子邮件地址",
|
||||||
"inviteValidityDuration": "请选择持续时间",
|
"inviteValidityDuration": "请选择持续时间",
|
||||||
"accessRoleSelectPlease": "请选择一个角色",
|
"accessRoleSelectPlease": "请选择一个角色",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "移除你的管理员权限?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "保存后,你将不再拥有该组织的管理员权限。如果需要,其他管理员可以恢复访问。",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "移除我的管理员访问权限",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "移除我的管理员访问",
|
||||||
|
"ownerMustRetainAdminRole": "组织所有者必须保留至少一个管理员角色。",
|
||||||
"usernameRequired": "必须输入用户名",
|
"usernameRequired": "必须输入用户名",
|
||||||
"idpSelectPlease": "请选择身份提供商",
|
"idpSelectPlease": "请选择身份提供商",
|
||||||
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
|
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
||||||
"targetsSubmit": "保存目标",
|
"targetsSubmit": "保存目标",
|
||||||
"addTarget": "添加目标",
|
"addTarget": "添加目标",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "轮询路由在未连接到相同节点的站点之间将不起作用,但故障转移会生效。",
|
||||||
"targetErrorInvalidIp": "无效的 IP 地址",
|
"targetErrorInvalidIp": "无效的 IP 地址",
|
||||||
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
||||||
"targetErrorInvalidPort": "无效的端口",
|
"targetErrorInvalidPort": "无效的端口",
|
||||||
@@ -1499,7 +1515,7 @@
|
|||||||
"alertingGraphCanvasTitle": "规则流程",
|
"alertingGraphCanvasTitle": "规则流程",
|
||||||
"alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。",
|
"alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。",
|
||||||
"alertingNodeNotConfigured": "尚未配置",
|
"alertingNodeNotConfigured": "尚未配置",
|
||||||
"alertingNodeActionsCount": "{count, plural, one {# action} other {# actions}}",
|
"alertingNodeActionsCount": "{count, plural, other {# 操作}}",
|
||||||
"alertingNodeRoleSource": "来源",
|
"alertingNodeRoleSource": "来源",
|
||||||
"alertingNodeRoleTrigger": "触发",
|
"alertingNodeRoleTrigger": "触发",
|
||||||
"alertingNodeRoleAction": "行为",
|
"alertingNodeRoleAction": "行为",
|
||||||
@@ -2051,7 +2067,7 @@
|
|||||||
"createInternalResourceDialogName": "名称",
|
"createInternalResourceDialogName": "名称",
|
||||||
"createInternalResourceDialogSite": "站点",
|
"createInternalResourceDialogSite": "站点",
|
||||||
"selectSite": "选择站点...",
|
"selectSite": "选择站点...",
|
||||||
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
|
"multiSitesSelectorSitesCount": "{count, plural, other {# 个网站}}",
|
||||||
"noSitesFound": "未找到站点。",
|
"noSitesFound": "未找到站点。",
|
||||||
"createInternalResourceDialogProtocol": "协议",
|
"createInternalResourceDialogProtocol": "协议",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "有效密码",
|
"validPassword": "有效密码",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "查看",
|
||||||
|
"configManaged": "配置已管理",
|
||||||
"connectedClient": "已连接客户端",
|
"connectedClient": "已连接客户端",
|
||||||
"resourceBlocked": "资源被阻止",
|
"resourceBlocked": "资源被阻止",
|
||||||
"droppedByRule": "被规则删除",
|
"droppedByRule": "被规则删除",
|
||||||
@@ -2672,7 +2690,7 @@
|
|||||||
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
||||||
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
||||||
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
||||||
"logRetentionRequestLabel": "请求日志保留",
|
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
||||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||||
"logRetentionAccessLabel": "访问日志保留",
|
"logRetentionAccessLabel": "访问日志保留",
|
||||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
|
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
|
||||||
"streamingTypePickerDescription": "选择要开始的目标类型。",
|
"streamingTypePickerDescription": "选择要开始的目标类型。",
|
||||||
"streamingFailedToLoad": "加载目的地失败",
|
"streamingLastSyncError": "最后一次同步时发生错误",
|
||||||
"streamingUnexpectedError": "发生意外错误.",
|
"streamingUnexpectedError": "发生意外错误.",
|
||||||
"streamingFailedToUpdate": "更新目标失败",
|
"streamingFailedToUpdate": "更新目标失败",
|
||||||
"streamingDeletedSuccess": "目标删除成功",
|
"streamingDeletedSuccess": "目标删除成功",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "编辑目的地",
|
"S3DestEditTitle": "编辑目的地",
|
||||||
"S3DestAddTitle": "添加 S3 目的地",
|
"S3DestAddTitle": "添加 S3 目的地",
|
||||||
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
|
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
|
||||||
"S3DestAddDescription": "配置新的 S3 终端以接收您的组织事件。",
|
"S3DestAddDescription": "配置一个新的 Amazon S3(或兼容 S3 的)存储桶以接收您的组织事件。",
|
||||||
|
"s3DestTabSettings": "设置",
|
||||||
|
"s3DestTabFormat": "格式",
|
||||||
|
"s3DestNameLabel": "名称",
|
||||||
|
"s3DestNamePlaceholder": "我的 S3 目的地",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS 访问密钥 ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS 秘密访问密钥",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "您的 AWS 密钥",
|
||||||
|
"s3DestRegionLabel": "AWS 地区",
|
||||||
|
"s3DestBucketLabel": "存储桶名称",
|
||||||
|
"s3DestPrefixLabel": "密钥前缀(可选)",
|
||||||
|
"s3DestPrefixDescription": "每个对象密钥前加的可选路径前缀。对象存储在 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}。",
|
||||||
|
"s3DestEndpointLabel": "自定义端点(可选)",
|
||||||
|
"s3DestEndpointDescription": "替代 S3 端点用于 MinIO 或 Cloudflare R2 等兼容 S3 的存储。标准 AWS S3 留空。",
|
||||||
|
"s3DestGzipLabel": "Gzip 压缩",
|
||||||
|
"s3DestGzipDescription": "使用 gzip 压缩每个上传的对象。减少存储成本和上传大小。",
|
||||||
|
"s3DestFormatTitle": "文件格式",
|
||||||
|
"s3DestFormatDescription": "事件在每个上传对象内的序列化方式。",
|
||||||
|
"s3DestFormatJsonArrayDescription": "每个对象是事件记录的 JSON 数组。兼容大多数分析工具。",
|
||||||
|
"s3DestFormatNdjsonDescription": "每个对象每行包含一个 JSON 记录(换行分隔的 JSON)。兼容 Athena、BigQuery 和 Spark。",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "每个对象是带有标题行的 RFC-4180 CSV 文件。列名来自事件数据字段。",
|
||||||
|
"s3DestSaveChanges": "保存更改",
|
||||||
|
"s3DestCreateDestination": "创建目的地",
|
||||||
|
"s3DestUpdatedSuccess": "目的地更新成功",
|
||||||
|
"s3DestCreatedSuccess": "目的地创建成功",
|
||||||
|
"s3DestUpdateFailed": "更新目的地失败",
|
||||||
|
"s3DestCreateFailed": "创建目的地失败",
|
||||||
"datadogDestEditTitle": "编辑目的地",
|
"datadogDestEditTitle": "编辑目的地",
|
||||||
"datadogDestAddTitle": "添加 Datadog 目的地",
|
"datadogDestAddTitle": "添加 Datadog 目的地",
|
||||||
"datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。",
|
"datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。",
|
||||||
@@ -3208,5 +3253,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": "下一页"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = pgTable(
|
|||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
clientEndpoint: text("clientEndpoint"),
|
||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
@@ -439,6 +440,8 @@ export const eventStreamingDestinations = pgTable(
|
|||||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: bigint("lastErrorAt", { mode: "number" }), // epoch ms of last error, null if healthy
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = sqliteTable(
|
|||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
clientEndpoint: text("clientEndpoint"),
|
||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
@@ -445,6 +446,8 @@ export const eventStreamingDestinations = sqliteTable(
|
|||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: integer("lastErrorAt"), // epoch ms of last error, null if healthy
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
updatedAt: integer("updatedAt").notNull()
|
updatedAt: integer("updatedAt").notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1227,7 +1227,11 @@ async function getDomainId(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainSelection = validDomains[0].domains;
|
// Pick the most specific (longest baseDomain) valid domain so that, e.g.,
|
||||||
|
// *.test.dev.example.com is assigned to *.dev.example.com rather than *.example.com.
|
||||||
|
const domainSelection = validDomains.sort(
|
||||||
|
(a, b) => b.domains.baseDomain.length - a.domains.baseDomain.length
|
||||||
|
)[0].domains;
|
||||||
const baseDomain = domainSelection.baseDomain;
|
const baseDomain = domainSelection.baseDomain;
|
||||||
|
|
||||||
// Wildcard full-domains are not allowed on namespace (provided/free) domains
|
// Wildcard full-domains are not allowed on namespace (provided/free) domains
|
||||||
|
|||||||
@@ -25,9 +25,162 @@ 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 adminRoleCache = new Map<
|
||||||
|
string,
|
||||||
|
typeof roles.$inferSelect | null
|
||||||
|
>();
|
||||||
|
const exitNodesCache = new Map<
|
||||||
|
string,
|
||||||
|
Awaited<ReturnType<typeof listExitNodes>>
|
||||||
|
>();
|
||||||
|
const isOrgLicensedCache = new Map<string, boolean>();
|
||||||
|
const existingClientCache = new Map<
|
||||||
|
string,
|
||||||
|
typeof clients.$inferSelect | null
|
||||||
|
>();
|
||||||
|
const roleClientAccessCache = new Map<string, boolean>();
|
||||||
|
const userClientAccessCache = new Map<string, boolean>();
|
||||||
|
|
||||||
|
const getOrgOlmKey = (orgId: string, olmId: string) =>
|
||||||
|
`${orgId}:${olmId}`;
|
||||||
|
const getRoleClientKey = (roleId: number, clientId: number) =>
|
||||||
|
`${roleId}:${clientId}`;
|
||||||
|
const getUserClientKey = (cachedUserId: string, clientId: number) =>
|
||||||
|
`${cachedUserId}:${clientId}`;
|
||||||
|
|
||||||
|
const getOrg = async (orgId: string) => {
|
||||||
|
if (orgCache.has(orgId)) {
|
||||||
|
return orgCache.get(orgId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [org] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
orgCache.set(orgId, org ?? null);
|
||||||
|
|
||||||
|
return org ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAdminRole = async (orgId: string) => {
|
||||||
|
if (adminRoleCache.has(orgId)) {
|
||||||
|
return adminRoleCache.get(orgId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [adminRole] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
adminRoleCache.set(orgId, adminRole ?? null);
|
||||||
|
|
||||||
|
return adminRole ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExitNodes = async (orgId: string) => {
|
||||||
|
if (exitNodesCache.has(orgId)) {
|
||||||
|
return exitNodesCache.get(orgId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodes = await listExitNodes(orgId);
|
||||||
|
exitNodesCache.set(orgId, exitNodes);
|
||||||
|
|
||||||
|
return exitNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIsOrgLicensed = async (orgId: string) => {
|
||||||
|
if (isOrgLicensedCache.has(orgId)) {
|
||||||
|
return isOrgLicensedCache.get(orgId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.deviceApprovals
|
||||||
|
);
|
||||||
|
isOrgLicensedCache.set(orgId, isOrgLicensed);
|
||||||
|
|
||||||
|
return isOrgLicensed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExistingClient = async (orgId: string, olmId: string) => {
|
||||||
|
const key = getOrgOlmKey(orgId, olmId);
|
||||||
|
if (existingClientCache.has(key)) {
|
||||||
|
return existingClientCache.get(key) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
eq(clients.orgId, orgId),
|
||||||
|
eq(clients.olmId, olmId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
existingClientCache.set(key, existingClient ?? null);
|
||||||
|
|
||||||
|
return existingClient ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasRoleClientAccess = async (
|
||||||
|
roleId: number,
|
||||||
|
clientId: number
|
||||||
|
) => {
|
||||||
|
const key = getRoleClientKey(roleId, clientId);
|
||||||
|
if (roleClientAccessCache.has(key)) {
|
||||||
|
return roleClientAccessCache.get(key)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingRoleClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(roleClients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleClients.roleId, roleId),
|
||||||
|
eq(roleClients.clientId, clientId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const hasAccess = Boolean(existingRoleClient);
|
||||||
|
roleClientAccessCache.set(key, hasAccess);
|
||||||
|
|
||||||
|
return hasAccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUserClientAccess = async (
|
||||||
|
cachedUserId: string,
|
||||||
|
clientId: number
|
||||||
|
) => {
|
||||||
|
const key = getUserClientKey(cachedUserId, clientId);
|
||||||
|
if (userClientAccessCache.has(key)) {
|
||||||
|
return userClientAccessCache.get(key)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingUserClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(userClients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userClients.userId, cachedUserId),
|
||||||
|
eq(userClients.clientId, clientId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const hasAccess = Boolean(existingUserClient);
|
||||||
|
userClientAccessCache.set(key, hasAccess);
|
||||||
|
|
||||||
|
return hasAccess;
|
||||||
|
};
|
||||||
|
|
||||||
// Get all OLMs for this user
|
// Get all OLMs for this user
|
||||||
const userOlms = await transaction
|
const userOlms = await transaction
|
||||||
.select()
|
.select()
|
||||||
@@ -54,7 +207,9 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(eq(userOrgs.userId, userId));
|
.where(eq(userOrgs.userId, userId));
|
||||||
|
|
||||||
const userOrgIds = [...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))];
|
const userOrgIds = [
|
||||||
|
...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
|
||||||
|
];
|
||||||
const orgIdToRoleRows = new Map<
|
const orgIdToRoleRows = new Map<
|
||||||
string,
|
string,
|
||||||
(typeof userOrgRoleRows)[0][]
|
(typeof userOrgRoleRows)[0][]
|
||||||
@@ -64,6 +219,13 @@ export async function calculateUserClientsForOrgs(
|
|||||||
list.push(r);
|
list.push(r);
|
||||||
orgIdToRoleRows.set(r.userOrgs.orgId, list);
|
orgIdToRoleRows.set(r.userOrgs.orgId, list);
|
||||||
}
|
}
|
||||||
|
const orgRequiresDeviceApprovalRole = new Map<string, boolean>();
|
||||||
|
for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
|
||||||
|
orgRequiresDeviceApprovalRole.set(
|
||||||
|
orgId,
|
||||||
|
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// For each OLM, ensure there's a client in each org the user is in
|
// For each OLM, ensure there's a client in each org the user is in
|
||||||
for (const olm of userOlms) {
|
for (const olm of userOlms) {
|
||||||
@@ -71,10 +233,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
|
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
|
||||||
const userOrg = roleRowsForOrg[0].userOrgs;
|
const userOrg = roleRowsForOrg[0].userOrgs;
|
||||||
|
|
||||||
const [org] = await transaction
|
const org = await getOrg(orgId);
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -91,11 +250,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get admin role for this org (needed for access grants)
|
// Get admin role for this org (needed for access grants)
|
||||||
const [adminRole] = await transaction
|
const adminRole = await getAdminRole(orgId);
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!adminRole) {
|
if (!adminRole) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -105,64 +260,50 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a client already exists for this OLM+user+org combination
|
// Check if a client already exists for this OLM+user+org combination
|
||||||
const [existingClient] = await transaction
|
const existingClient = await getExistingClient(
|
||||||
.select()
|
orgId,
|
||||||
.from(clients)
|
olm.olmId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(clients.userId, userId),
|
|
||||||
eq(clients.orgId, orgId),
|
|
||||||
eq(clients.olmId, olm.olmId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingClient) {
|
if (existingClient) {
|
||||||
// Ensure admin role has access to the client
|
// Ensure admin role has access to the client
|
||||||
const [existingRoleClient] = await transaction
|
const hasRoleAccess = await hasRoleClientAccess(
|
||||||
.select()
|
adminRole.roleId,
|
||||||
.from(roleClients)
|
existingClient.clientId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(roleClients.roleId, adminRole.roleId),
|
|
||||||
eq(
|
|
||||||
roleClients.clientId,
|
|
||||||
existingClient.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!existingRoleClient) {
|
if (!hasRoleAccess) {
|
||||||
await transaction.insert(roleClients).values({
|
await transaction.insert(roleClients).values({
|
||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
clientId: existingClient.clientId
|
clientId: existingClient.clientId
|
||||||
});
|
});
|
||||||
|
roleClientAccessCache.set(
|
||||||
|
getRoleClientKey(
|
||||||
|
adminRole.roleId,
|
||||||
|
existingClient.clientId
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user has access to the client
|
// Ensure user has access to the client
|
||||||
const [existingUserClient] = await transaction
|
const hasUserAccess = await hasUserClientAccess(
|
||||||
.select()
|
userId,
|
||||||
.from(userClients)
|
existingClient.clientId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(userClients.userId, userId),
|
|
||||||
eq(
|
|
||||||
userClients.clientId,
|
|
||||||
existingClient.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!existingUserClient) {
|
if (!hasUserAccess) {
|
||||||
await transaction.insert(userClients).values({
|
await transaction.insert(userClients).values({
|
||||||
userId,
|
userId,
|
||||||
clientId: existingClient.clientId
|
clientId: existingClient.clientId
|
||||||
});
|
});
|
||||||
|
userClientAccessCache.set(
|
||||||
|
getUserClientKey(userId, existingClient.clientId),
|
||||||
|
true
|
||||||
|
);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
);
|
);
|
||||||
@@ -175,7 +316,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get exit nodes for this org
|
// Get exit nodes for this org
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
const exitNodesList = await getExitNodes(orgId);
|
||||||
|
|
||||||
if (exitNodesList.length === 0) {
|
if (exitNodesList.length === 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -206,14 +347,11 @@ export async function calculateUserClientsForOrgs(
|
|||||||
|
|
||||||
const niceId = await getUniqueClientName(orgId);
|
const niceId = await getUniqueClientName(orgId);
|
||||||
|
|
||||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
|
||||||
userOrg.orgId,
|
|
||||||
tierMatrix.deviceApprovals
|
|
||||||
);
|
|
||||||
const requireApproval =
|
const requireApproval =
|
||||||
build !== "oss" &&
|
build !== "oss" &&
|
||||||
isOrgLicensed &&
|
isOrgLicensed &&
|
||||||
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval);
|
orgRequiresDeviceApprovalRole.get(orgId) === true;
|
||||||
|
|
||||||
const newClientData: InferInsertModel<typeof clients> = {
|
const newClientData: InferInsertModel<typeof clients> = {
|
||||||
userId,
|
userId,
|
||||||
@@ -232,6 +370,10 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values(newClientData)
|
.values(newClientData)
|
||||||
.returning();
|
.returning();
|
||||||
|
existingClientCache.set(
|
||||||
|
getOrgOlmKey(orgId, olm.olmId),
|
||||||
|
newClient
|
||||||
|
);
|
||||||
|
|
||||||
// create approval request
|
// create approval request
|
||||||
if (requireApproval) {
|
if (requireApproval) {
|
||||||
@@ -257,12 +399,20 @@ export async function calculateUserClientsForOrgs(
|
|||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
|
roleClientAccessCache.set(
|
||||||
|
getRoleClientKey(adminRole.roleId, newClient.clientId),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Grant user access to the client
|
// Grant user access to the client
|
||||||
await transaction.insert(userClients).values({
|
await transaction.insert(userClients).values({
|
||||||
userId,
|
userId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
|
userClientAccessCache.set(
|
||||||
|
getUserClientKey(userId, newClient.clientId),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
|
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
|
||||||
@@ -287,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.4";
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
212
server/lib/ip.ts
212
server/lib/ip.ts
@@ -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,120 +328,146 @@ export async function getNextAvailableClientSubnet(
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
transaction: Transaction | typeof db = db
|
transaction: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [org] = await transaction
|
return await lockManager.withLock(
|
||||||
.select()
|
`client-subnet-allocation:${orgId}`,
|
||||||
.from(orgs)
|
async () => {
|
||||||
.where(eq(orgs.orgId, orgId));
|
const [org] = await transaction
|
||||||
|
.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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingAddressesSites = await transaction
|
const existingAddressesSites = await transaction
|
||||||
.select({
|
.select({
|
||||||
address: sites.address
|
address: sites.address
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||||
|
|
||||||
const existingAddressesClients = await transaction
|
const existingAddressesClients = await transaction
|
||||||
.select({
|
.select({
|
||||||
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(
|
||||||
(site) => `${site.address?.split("/")[0]}/32`
|
(site) => `${site.address?.split("/")[0]}/32`
|
||||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||||
...existingAddressesClients.map(
|
...existingAddressesClients.map(
|
||||||
(client) => `${client.address.split("/")}/32`
|
(client) => `${client.address.split("/")}/32`
|
||||||
)
|
)
|
||||||
].filter((address) => address !== null) as string[];
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Organization with ID ${orgId} has no utility subnet defined`
|
`Organization with ID ${orgId} has no utility subnet defined`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAddresses = await db
|
const existingAddresses = await trx
|
||||||
.select({
|
.select({
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
isNotNull(siteResources.aliasAddress),
|
isNotNull(siteResources.aliasAddress),
|
||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const addresses = [
|
const addresses = [
|
||||||
...existingAddresses.map(
|
...existingAddresses.map(
|
||||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||||
),
|
),
|
||||||
// reserve a /29 for the dns server and other stuff
|
// reserve a /29 for the dns server and other stuff
|
||||||
`${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(
|
||||||
if (!subnet) {
|
addresses,
|
||||||
throw new Error("No available subnets remaining in space");
|
32,
|
||||||
}
|
org.utilitySubnet
|
||||||
|
);
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
// remove the cidr
|
// remove the cidr
|
||||||
subnet = subnet.split("/")[0];
|
subnet = subnet.split("/")[0];
|
||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||||
const existingAddresses = await db
|
return await lockManager.withLock("org-subnet-allocation", async () => {
|
||||||
.select({
|
const existingAddresses = await db
|
||||||
subnet: orgs.subnet
|
.select({
|
||||||
})
|
subnet: orgs.subnet
|
||||||
.from(orgs)
|
})
|
||||||
.where(isNotNull(orgs.subnet));
|
.from(orgs)
|
||||||
|
.where(isNotNull(orgs.subnet));
|
||||||
|
|
||||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||||
|
|
||||||
const subnet = findNextAvailableCidr(
|
const subnet = findNextAvailableCidr(
|
||||||
addresses,
|
addresses,
|
||||||
config.getRawConfig().orgs.block_size,
|
config.getRawConfig().orgs.block_size,
|
||||||
config.getRawConfig().orgs.subnet_group
|
config.getRawConfig().orgs.subnet_group
|
||||||
);
|
);
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, logsDb, statusHistory } from "@server/db";
|
import { db, logsDb, statusHistory } from "@server/db";
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
import { and, eq, gte, asc } from "drizzle-orm";
|
||||||
import cache from "@server/lib/cache";
|
import { regionalCache as cache } from "@server/private/lib/cache";
|
||||||
|
|
||||||
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -63,7 +66,7 @@ export async function invalidateStatusHistoryCache(
|
|||||||
entityId: number
|
entityId: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const prefix = `statusHistory:${entityType}:${entityId}:`;
|
const prefix = `statusHistory:${entityType}:${entityId}:`;
|
||||||
const keys = cache.keys().filter((k) => k.startsWith(prefix));
|
const keys = await cache.keysWithPrefix(prefix);
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) {
|
||||||
await cache.del(keys);
|
await cache.del(keys);
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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[];
|
||||||
@@ -500,7 +627,30 @@ function findAcmeJsonFiles(dirPath: string): string[] {
|
|||||||
const fullPath = path.join(dirPath, entry.name);
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
results.push(...findAcmeJsonFiles(fullPath));
|
results.push(...findAcmeJsonFiles(fullPath));
|
||||||
} else if (entry.isFile() && entry.name === "acme.json") {
|
} else if (entry.isFile()) {
|
||||||
|
// check if it is a json file
|
||||||
|
if (entry.name.endsWith(".json")) {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = fs.readFileSync(fullPath, "utf8");
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not read file "${fullPath}": ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: any;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not parse "${fullPath}" as JSON: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results.push(fullPath);
|
results.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,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;
|
||||||
}
|
}
|
||||||
@@ -575,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;
|
||||||
}
|
}
|
||||||
@@ -593,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;
|
||||||
}
|
}
|
||||||
@@ -605,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;
|
||||||
}
|
}
|
||||||
@@ -615,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(
|
|
||||||
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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
|
logger.debug(
|
||||||
let expiresAt: number | null = null;
|
`acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
|
||||||
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);
|
for (const domain of allDomains) {
|
||||||
if (domainId) {
|
try {
|
||||||
logger.debug(
|
await storeCertForDomain(
|
||||||
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
domain,
|
||||||
);
|
certPem,
|
||||||
} else {
|
keyPem,
|
||||||
logger.debug(
|
validatedX509
|
||||||
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
);
|
||||||
);
|
} catch (err) {
|
||||||
}
|
logger.error(
|
||||||
|
`acmeCertSync: error storing cert for domain "${domain}": ${err}`
|
||||||
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));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { redisManager } from "@server/private/lib/redis";
|
import { redisManager, regionalRedisManager } from "@server/private/lib/redis";
|
||||||
|
|
||||||
// Create local cache with maxKeys limit to prevent memory leaks
|
// Create local cache with maxKeys limit to prevent memory leaks
|
||||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||||
@@ -298,3 +298,147 @@ class AdaptiveCache {
|
|||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const cache = new AdaptiveCache();
|
export const cache = new AdaptiveCache();
|
||||||
export default cache;
|
export default cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regional adaptive cache backed by the in-cluster Redis instance.
|
||||||
|
* Falls back to a local NodeCache when the regional Redis is unavailable.
|
||||||
|
* Use this for data that is regional in nature (e.g. status history) so
|
||||||
|
* reads are served from the same cluster the user is hitting.
|
||||||
|
*/
|
||||||
|
const regionalLocalCache = new NodeCache({
|
||||||
|
stdTTL: 3600,
|
||||||
|
checkperiod: 120,
|
||||||
|
maxKeys: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
class RegionalAdaptiveCache {
|
||||||
|
private useRedis(): boolean {
|
||||||
|
return (
|
||||||
|
regionalRedisManager.isRedisEnabled() &&
|
||||||
|
regionalRedisManager.getHealthStatus().isHealthy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: any, ttl?: number): Promise<boolean> {
|
||||||
|
const effectiveTtl = ttl === 0 ? undefined : ttl;
|
||||||
|
const redisTtl = ttl === 0 ? undefined : (ttl ?? 3600);
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
const success = await regionalRedisManager.set(
|
||||||
|
key,
|
||||||
|
serialized,
|
||||||
|
redisTtl
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
logger.debug(`[regional] Set key in Redis: ${key}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis set error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = regionalLocalCache.set(key, value, effectiveTtl || 0);
|
||||||
|
if (success) logger.debug(`[regional] Set key in local cache: ${key}`);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T = any>(key: string): Promise<T | undefined> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await regionalRedisManager.get(key);
|
||||||
|
if (value !== null) {
|
||||||
|
logger.debug(`[regional] Cache hit in Redis: ${key}`);
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
}
|
||||||
|
logger.debug(`[regional] Cache miss in Redis: ${key}`);
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis get error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = regionalLocalCache.get<T>(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
logger.debug(`[regional] Cache hit in local cache: ${key}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`[regional] Cache miss in local cache: ${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string | string[]): Promise<number> {
|
||||||
|
const keys = Array.isArray(key) ? key : [key];
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
for (const k of keys) {
|
||||||
|
const success = await regionalRedisManager.del(k);
|
||||||
|
if (success) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`[regional] Deleted key from Redis: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deletedCount === keys.length) return deletedCount;
|
||||||
|
deletedCount = 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[regional] Redis del error:`, error);
|
||||||
|
deletedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
const count = regionalLocalCache.del(k);
|
||||||
|
if (count > 0) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`[regional] Deleted key from local cache: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async has(key: string): Promise<boolean> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await regionalRedisManager.get(key);
|
||||||
|
return value !== null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis has error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regionalLocalCache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keys matching the given prefix from whichever backend is active.
|
||||||
|
* Redis uses a KEYS scan; local cache filters in-memory keys.
|
||||||
|
*/
|
||||||
|
async keysWithPrefix(prefix: string): Promise<string[]> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
return await regionalRedisManager.keys(`${prefix}*`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[regional] Redis keys error:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regionalLocalCache.keys().filter((k) => k.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentBackend(): "redis" | "local" {
|
||||||
|
return this.useRedis() ? "redis" : "local";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regionalCache = new RegionalAdaptiveCache();
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ export class PrivateConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.env.BRANDING_HIDE_POWERED_BY =
|
||||||
|
this.rawPrivateConfig.branding?.hide_powered_by === true ||
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page
|
||||||
|
?.hide_powered_by === true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
||||||
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export interface ConnectionLogRecord {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
sourceAddr: string;
|
sourceAddr: string;
|
||||||
destAddr: string;
|
destAddr: string;
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ import {
|
|||||||
LOG_TYPES,
|
LOG_TYPES,
|
||||||
LogEvent,
|
LogEvent,
|
||||||
DestinationFailureState,
|
DestinationFailureState,
|
||||||
HttpConfig
|
HttpConfig,
|
||||||
|
S3Config
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { LogDestinationProvider } from "./providers/LogDestinationProvider";
|
import { LogDestinationProvider } from "./providers/LogDestinationProvider";
|
||||||
import { HttpLogDestination } from "./providers/HttpLogDestination";
|
import { HttpLogDestination } from "./providers/HttpLogDestination";
|
||||||
|
import { S3LogDestination } from "./providers/S3LogDestination";
|
||||||
import type { EventStreamingDestination } from "@server/db";
|
import type { EventStreamingDestination } from "@server/db";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -72,11 +74,11 @@ const MAX_CATCHUP_BATCHES = 20;
|
|||||||
* After the last entry the max value is re-used.
|
* After the last entry the max value is re-used.
|
||||||
*/
|
*/
|
||||||
const BACKOFF_SCHEDULE_MS = [
|
const BACKOFF_SCHEDULE_MS = [
|
||||||
60_000, // 1 min (failure 1)
|
60_000, // 1 min (failure 1)
|
||||||
2 * 60_000, // 2 min (failure 2)
|
2 * 60_000, // 2 min (failure 2)
|
||||||
5 * 60_000, // 5 min (failure 3)
|
5 * 60_000, // 5 min (failure 3)
|
||||||
10 * 60_000, // 10 min (failure 4)
|
10 * 60_000, // 10 min (failure 4)
|
||||||
30 * 60_000 // 30 min (failure 5+)
|
30 * 60_000 // 30 min (failure 5+)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,7 +206,10 @@ export class LogStreamingManager {
|
|||||||
this.pollTimer = null;
|
this.pollTimer = null;
|
||||||
this.runPoll()
|
this.runPoll()
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
logger.error("LogStreamingManager: unexpected poll error", err)
|
logger.error(
|
||||||
|
"LogStreamingManager: unexpected poll error",
|
||||||
|
err
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (this.isRunning) {
|
if (this.isRunning) {
|
||||||
@@ -275,10 +280,13 @@ export class LogStreamingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt and parse config – skip destination if either step fails
|
// Decrypt and parse config – skip destination if either step fails
|
||||||
let configFromDb: HttpConfig;
|
let configFromDb: unknown;
|
||||||
try {
|
try {
|
||||||
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
|
const decryptedConfig = decrypt(
|
||||||
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
|
dest.config,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
configFromDb = JSON.parse(decryptedConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
||||||
@@ -305,6 +313,7 @@ export class LogStreamingManager {
|
|||||||
if (enabledTypes.length === 0) return;
|
if (enabledTypes.length === 0) return;
|
||||||
|
|
||||||
let anyFailure = false;
|
let anyFailure = false;
|
||||||
|
let firstError: string | null = null;
|
||||||
|
|
||||||
for (const logType of enabledTypes) {
|
for (const logType of enabledTypes) {
|
||||||
if (!this.isRunning) break;
|
if (!this.isRunning) break;
|
||||||
@@ -312,6 +321,10 @@ export class LogStreamingManager {
|
|||||||
await this.processLogType(dest, provider, logType);
|
await this.processLogType(dest, provider, logType);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
anyFailure = true;
|
anyFailure = true;
|
||||||
|
if (firstError === null) {
|
||||||
|
firstError =
|
||||||
|
err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
`LogStreamingManager: failed to process "${logType}" logs ` +
|
`LogStreamingManager: failed to process "${logType}" logs ` +
|
||||||
`for destination ${dest.destinationId}`,
|
`for destination ${dest.destinationId}`,
|
||||||
@@ -322,6 +335,10 @@ export class LogStreamingManager {
|
|||||||
|
|
||||||
if (anyFailure) {
|
if (anyFailure) {
|
||||||
this.recordFailure(dest.destinationId);
|
this.recordFailure(dest.destinationId);
|
||||||
|
await this.setDestinationError(
|
||||||
|
dest.destinationId,
|
||||||
|
firstError ?? "Unknown error"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Any success resets the failure/back-off state
|
// Any success resets the failure/back-off state
|
||||||
if (this.failures.has(dest.destinationId)) {
|
if (this.failures.has(dest.destinationId)) {
|
||||||
@@ -330,6 +347,7 @@ export class LogStreamingManager {
|
|||||||
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.clearDestinationError(dest.destinationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +380,10 @@ export class LogStreamingManager {
|
|||||||
.from(eventStreamingCursors)
|
.from(eventStreamingCursors)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(eventStreamingCursors.destinationId, dest.destinationId),
|
eq(
|
||||||
|
eventStreamingCursors.destinationId,
|
||||||
|
dest.destinationId
|
||||||
|
),
|
||||||
eq(eventStreamingCursors.logType, logType)
|
eq(eventStreamingCursors.logType, logType)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -431,9 +452,7 @@ export class LogStreamingManager {
|
|||||||
|
|
||||||
if (rows.length === 0) break;
|
if (rows.length === 0) break;
|
||||||
|
|
||||||
const events = rows.map((row) =>
|
const events = rows.map((row) => this.rowToLogEvent(logType, row));
|
||||||
this.rowToLogEvent(logType, row)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Throws on failure – caught by the caller which applies back-off
|
// Throws on failure – caught by the caller which applies back-off
|
||||||
await provider.send(events);
|
await provider.send(events);
|
||||||
@@ -677,8 +696,7 @@ export class LogStreamingManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgId =
|
const orgId = typeof row.orgId === "string" ? row.orgId : "";
|
||||||
typeof row.orgId === "string" ? row.orgId : "";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -708,6 +726,8 @@ export class LogStreamingManager {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "http":
|
case "http":
|
||||||
return new HttpLogDestination(config as HttpConfig);
|
return new HttpLogDestination(config as HttpConfig);
|
||||||
|
case "s3":
|
||||||
|
return new S3LogDestination(config as S3Config);
|
||||||
// Future providers:
|
// Future providers:
|
||||||
// case "datadog": return new DatadogLogDestination(config as DatadogConfig);
|
// case "datadog": return new DatadogLogDestination(config as DatadogConfig);
|
||||||
default:
|
default:
|
||||||
@@ -749,6 +769,45 @@ export class LogStreamingManager {
|
|||||||
// DB helpers
|
// DB helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private async setDestinationError(
|
||||||
|
destinationId: number,
|
||||||
|
errorMessage: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Truncate to 1000 chars so it fits comfortably in the text column.
|
||||||
|
const truncated = errorMessage.slice(0, 1000);
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: truncated, lastErrorAt: Date.now() })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not persist error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearDestinationError(destinationId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Only update if there is actually an error stored, to avoid
|
||||||
|
// unnecessary writes on every successful poll cycle.
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: null, lastErrorAt: null })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not clear error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadEnabledDestinations(): Promise<
|
private async loadEnabledDestinations(): Promise<
|
||||||
EventStreamingDestination[]
|
EventStreamingDestination[]
|
||||||
> {
|
> {
|
||||||
|
|||||||
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
* 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 { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { gzip as gzipCallback } from "zlib";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { LogEvent, S3Config, S3PayloadFormat } from "../types";
|
||||||
|
import { LogDestinationProvider } from "./LogDestinationProvider";
|
||||||
|
|
||||||
|
const gzipAsync = promisify(gzipCallback);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Maximum time (ms) to wait for a single S3 PutObject response. */
|
||||||
|
const REQUEST_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
|
/** Default payload format when none is specified in the config. */
|
||||||
|
const DEFAULT_FORMAT: S3PayloadFormat = "json_array";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// S3LogDestination
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwards a batch of log events to an S3-compatible object store by
|
||||||
|
* uploading a single object per `send()` call.
|
||||||
|
*
|
||||||
|
* **Object key layout**
|
||||||
|
* ```
|
||||||
|
* {prefix}/{logType}/{YYYY}/{MM}/{DD}/{HH}-{mm}-{ss}-{uuid}.{ext}[.gz]
|
||||||
|
* ```
|
||||||
|
* - `prefix` – from `config.prefix` (default: empty – key starts at logType)
|
||||||
|
* - `logType` – one of "request", "action", "access", "connection"
|
||||||
|
* - Date components are derived from the upload time (UTC)
|
||||||
|
* - `ext` – `json` | `ndjson` | `csv`
|
||||||
|
* - `.gz` – appended when `config.gzip` is true
|
||||||
|
*
|
||||||
|
* **Payload formats** (controlled by `config.format`):
|
||||||
|
* - `json_array` (default) – body is a JSON array of event objects.
|
||||||
|
* - `ndjson` – one JSON object per line (newline-delimited).
|
||||||
|
* - `csv` – RFC-4180 CSV with a header row; columns are the
|
||||||
|
* union of all field names in the batch's event data.
|
||||||
|
*
|
||||||
|
* **Compression**: when `config.gzip` is `true` the body is gzip-compressed
|
||||||
|
* before upload and `Content-Encoding: gzip` is set on the object.
|
||||||
|
*
|
||||||
|
* **Custom endpoint**: set `config.endpoint` to target any S3-compatible
|
||||||
|
* storage service (e.g. MinIO, Cloudflare R2).
|
||||||
|
*/
|
||||||
|
export class S3LogDestination implements LogDestinationProvider {
|
||||||
|
readonly type = "s3";
|
||||||
|
|
||||||
|
private readonly config: S3Config;
|
||||||
|
|
||||||
|
constructor(config: S3Config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// LogDestinationProvider implementation
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
async send(events: LogEvent[]): Promise<void> {
|
||||||
|
if (events.length === 0) return;
|
||||||
|
|
||||||
|
const format = this.config.format ?? DEFAULT_FORMAT;
|
||||||
|
const useGzip = this.config.gzip ?? false;
|
||||||
|
const logType = events[0].logType;
|
||||||
|
|
||||||
|
const rawBody = this.serialize(events, format);
|
||||||
|
const bodyBuffer = Buffer.from(rawBody, "utf-8");
|
||||||
|
|
||||||
|
let uploadBody: Buffer;
|
||||||
|
let contentEncoding: string | undefined;
|
||||||
|
|
||||||
|
if (useGzip) {
|
||||||
|
uploadBody = (await gzipAsync(bodyBuffer)) as Buffer;
|
||||||
|
contentEncoding = "gzip";
|
||||||
|
} else {
|
||||||
|
uploadBody = bodyBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.buildObjectKey(logType, format, useGzip);
|
||||||
|
const contentType = this.contentType(format);
|
||||||
|
|
||||||
|
const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
|
||||||
|
region: this.config.region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.config.accessKeyId,
|
||||||
|
secretAccessKey: this.config.secretAccessKey
|
||||||
|
},
|
||||||
|
requestHandler: {
|
||||||
|
requestTimeout: REQUEST_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.endpoint?.trim()) {
|
||||||
|
clientConfig.endpoint = this.config.endpoint.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new S3Client(clientConfig);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: this.config.bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: uploadBody,
|
||||||
|
ContentType: contentType,
|
||||||
|
...(contentEncoding
|
||||||
|
? { ContentEncoding: contentEncoding }
|
||||||
|
: {})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(
|
||||||
|
`S3LogDestination: failed to upload object "${key}" ` +
|
||||||
|
`to bucket "${this.config.bucket}" – ${msg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a unique S3 object key for the given log type and format.
|
||||||
|
* Keys are partitioned by logType and date so they can be queried or
|
||||||
|
* lifecycle-managed independently.
|
||||||
|
*/
|
||||||
|
private buildObjectKey(
|
||||||
|
logType: string,
|
||||||
|
format: S3PayloadFormat,
|
||||||
|
gzip: boolean
|
||||||
|
): string {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getUTCFullYear();
|
||||||
|
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getUTCDate()).padStart(2, "0");
|
||||||
|
const hh = String(now.getUTCHours()).padStart(2, "0");
|
||||||
|
const mm = String(now.getUTCMinutes()).padStart(2, "0");
|
||||||
|
const ss = String(now.getUTCSeconds()).padStart(2, "0");
|
||||||
|
const uid = randomUUID();
|
||||||
|
|
||||||
|
const ext =
|
||||||
|
format === "csv" ? "csv" : format === "ndjson" ? "ndjson" : "json";
|
||||||
|
const fileName = `${hh}-${mm}-${ss}-${uid}.${ext}${gzip ? ".gz" : ""}`;
|
||||||
|
|
||||||
|
const rawPrefix = (this.config.prefix ?? "").trim().replace(/\/+$/, "");
|
||||||
|
const parts = [
|
||||||
|
rawPrefix,
|
||||||
|
logType,
|
||||||
|
`${year}/${month}/${day}`,
|
||||||
|
fileName
|
||||||
|
].filter((p) => p !== "");
|
||||||
|
|
||||||
|
return parts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentType(format: S3PayloadFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case "csv":
|
||||||
|
return "text/csv; charset=utf-8";
|
||||||
|
case "ndjson":
|
||||||
|
return "application/x-ndjson";
|
||||||
|
default:
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private serialize(events: LogEvent[], format: S3PayloadFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case "json_array":
|
||||||
|
return JSON.stringify(events.map(toPayload));
|
||||||
|
case "ndjson":
|
||||||
|
return events
|
||||||
|
.map((e) => JSON.stringify(toPayload(e)))
|
||||||
|
.join("\n");
|
||||||
|
case "csv":
|
||||||
|
return toCsv(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Payload helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function toPayload(event: LogEvent): unknown {
|
||||||
|
return {
|
||||||
|
event: event.logType,
|
||||||
|
timestamp: new Date(event.timestamp * 1000).toISOString(),
|
||||||
|
data: event.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a batch of events to RFC-4180 CSV.
|
||||||
|
*
|
||||||
|
* The column set is the union of `event`, `timestamp`, and all keys present in
|
||||||
|
* `event.data` across the batch, preserving insertion order. Values that
|
||||||
|
* contain commas, double-quotes, or newlines are quoted and escaped.
|
||||||
|
*/
|
||||||
|
function toCsv(events: LogEvent[]): string {
|
||||||
|
if (events.length === 0) return "";
|
||||||
|
|
||||||
|
// Collect all unique data keys in stable order
|
||||||
|
const keySet = new LinkedSet<string>();
|
||||||
|
keySet.add("event");
|
||||||
|
keySet.add("timestamp");
|
||||||
|
for (const e of events) {
|
||||||
|
for (const k of Object.keys(e.data)) {
|
||||||
|
keySet.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const headers = keySet.toArray();
|
||||||
|
|
||||||
|
const rows: string[] = [headers.map(csvEscape).join(",")];
|
||||||
|
|
||||||
|
for (const e of events) {
|
||||||
|
const flat: Record<string, unknown> = {
|
||||||
|
event: e.logType,
|
||||||
|
timestamp: new Date(e.timestamp * 1000).toISOString(),
|
||||||
|
...e.data
|
||||||
|
};
|
||||||
|
rows.push(
|
||||||
|
headers.map((h) => csvEscape(flattenValue(flat[h]))).join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten a value to a plain string suitable for a CSV cell. */
|
||||||
|
function flattenValue(value: unknown): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
if (typeof value === "object") return JSON.stringify(value);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RFC-4180 CSV escaping. */
|
||||||
|
function csvEscape(value: string): string {
|
||||||
|
if (/[",\n\r]/.test(value)) {
|
||||||
|
return `"${value.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal ordered set (preserves insertion order, deduplicates)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class LinkedSet<T> {
|
||||||
|
private readonly map = new Map<T, true>();
|
||||||
|
|
||||||
|
add(value: T): void {
|
||||||
|
this.map.set(value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return Array.from(this.map.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,40 @@ export interface HttpConfig {
|
|||||||
bodyTemplate?: string;
|
bodyTemplate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// S3 destination configuration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls how the batch of events is serialised into each S3 object.
|
||||||
|
*
|
||||||
|
* - `json_array` – `[{…}, {…}]` – default; each object is a JSON array.
|
||||||
|
* - `ndjson` – `{…}\n{…}` – newline-delimited JSON, one object per line.
|
||||||
|
* - `csv` – RFC-4180 CSV with a header row derived from the event fields.
|
||||||
|
*/
|
||||||
|
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
/** Human-readable label for the destination */
|
||||||
|
name: string;
|
||||||
|
/** AWS Access Key ID */
|
||||||
|
accessKeyId: string;
|
||||||
|
/** AWS Secret Access Key */
|
||||||
|
secretAccessKey: string;
|
||||||
|
/** AWS region (e.g. "us-east-1") */
|
||||||
|
region: string;
|
||||||
|
/** Target S3 bucket name */
|
||||||
|
bucket: string;
|
||||||
|
/** Optional key prefix – appended before the auto-generated path */
|
||||||
|
prefix?: string;
|
||||||
|
/** Override the S3 endpoint for S3-compatible storage (e.g. MinIO, R2) */
|
||||||
|
endpoint?: string;
|
||||||
|
/** How events are serialised into each object. Defaults to "json_array". */
|
||||||
|
format: S3PayloadFormat;
|
||||||
|
/** Whether to gzip-compress the object before upload. */
|
||||||
|
gzip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Per-destination per-log-type cursor (reflects the DB table)
|
// Per-destination per-log-type cursor (reflects the DB table)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ export const privateConfigSchema = z
|
|||||||
.object({
|
.object({
|
||||||
rejectUnauthorized: z.boolean().optional().default(true)
|
rejectUnauthorized: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
|
.optional(),
|
||||||
|
regional_redis: z
|
||||||
|
.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: portSchema,
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(getEnvOrYaml("REGIONAL_REDIS_PASSWORD")),
|
||||||
|
db: z.int().nonnegative().optional().default(0),
|
||||||
|
tls: z
|
||||||
|
.object({
|
||||||
|
rejectUnauthorized: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -141,6 +160,7 @@ export const privateConfigSchema = z
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
hide_auth_layout_footer: z.boolean().optional().default(false),
|
hide_auth_layout_footer: z.boolean().optional().default(false),
|
||||||
|
hide_powered_by: z.boolean().optional(),
|
||||||
login_page: z
|
login_page: z
|
||||||
.object({
|
.object({
|
||||||
subtitle_text: z.string().optional()
|
subtitle_text: z.string().optional()
|
||||||
|
|||||||
@@ -109,14 +109,14 @@ class RedisManager {
|
|||||||
password: redisConfig.password,
|
password: redisConfig.password,
|
||||||
db: redisConfig.db
|
db: redisConfig.db
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
if (redisConfig.tls) {
|
if (redisConfig.tls) {
|
||||||
opts.tls = {
|
opts.tls = {
|
||||||
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,14 +135,14 @@ class RedisManager {
|
|||||||
password: replica.password,
|
password: replica.password,
|
||||||
db: replica.db || redisConfig.db
|
db: replica.db || redisConfig.db
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
if (redisConfig.tls) {
|
if (redisConfig.tls) {
|
||||||
opts.tls = {
|
opts.tls = {
|
||||||
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,3 +855,163 @@ class RedisManager {
|
|||||||
export const redisManager = new RedisManager();
|
export const redisManager = new RedisManager();
|
||||||
export const redis = redisManager.getClient();
|
export const redis = redisManager.getClient();
|
||||||
export default redisManager;
|
export default redisManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight Redis manager for the regional (in-cluster) Redis instance.
|
||||||
|
* Connects only when `redis.regional_redis` is present in the private config
|
||||||
|
* and `flags.enable_redis` is true. No pub/sub — designed for low-latency
|
||||||
|
* caching of regionally-scoped data.
|
||||||
|
*/
|
||||||
|
class RegionalRedisManager {
|
||||||
|
private writeClient: Redis | null = null;
|
||||||
|
private readClient: Redis | null = null;
|
||||||
|
private isEnabled: boolean = false;
|
||||||
|
private isHealthy: boolean = false;
|
||||||
|
private connectionTimeout: number = 5000;
|
||||||
|
private commandTimeout: number = 5000;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (build === "oss") return;
|
||||||
|
|
||||||
|
const cfg = privateConfig.getRawPrivateConfig();
|
||||||
|
if (!cfg.flags.enable_redis || !cfg.redis?.regional_redis) return;
|
||||||
|
|
||||||
|
this.isEnabled = true;
|
||||||
|
this.initializeClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfig(): RedisOptions {
|
||||||
|
const r = privateConfig.getRawPrivateConfig().redis!.regional_redis!;
|
||||||
|
const opts: RedisOptions = {
|
||||||
|
host: r.host,
|
||||||
|
port: r.port,
|
||||||
|
password: r.password,
|
||||||
|
db: r.db
|
||||||
|
};
|
||||||
|
if (r.tls) {
|
||||||
|
opts.tls = { rejectUnauthorized: r.tls.rejectUnauthorized ?? true };
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeClients(): void {
|
||||||
|
const cfg = this.getConfig();
|
||||||
|
const baseOpts = {
|
||||||
|
...cfg,
|
||||||
|
enableReadyCheck: false,
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
keepAlive: 10000,
|
||||||
|
connectTimeout: this.connectionTimeout,
|
||||||
|
commandTimeout: this.commandTimeout
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.writeClient = new Redis(baseOpts);
|
||||||
|
// redis-1 (replica) handles reads; fall back to primary if not resolvable
|
||||||
|
this.readClient = new Redis({
|
||||||
|
...baseOpts,
|
||||||
|
host: cfg.host!.replace(/^(.*?)(\.\S+)$/, (_, h, rest) => {
|
||||||
|
// Derive replica hostname from the headless service pattern:
|
||||||
|
// redis.redis.svc.cluster.local -> redis-1.redis-headless.redis.svc.cluster.local
|
||||||
|
// If it doesn't look like a k8s service, just use the same host
|
||||||
|
return h + rest;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// For simplicity use same host for both; callers can always read from primary
|
||||||
|
// The real replica routing is handled by the StatefulSet headless service
|
||||||
|
this.readClient = this.writeClient;
|
||||||
|
|
||||||
|
this.writeClient.on("ready", () => {
|
||||||
|
logger.info("Regional Redis client ready");
|
||||||
|
this.isHealthy = true;
|
||||||
|
});
|
||||||
|
this.writeClient.on("error", (err) => {
|
||||||
|
logger.error("Regional Redis client error:", err);
|
||||||
|
this.isHealthy = false;
|
||||||
|
});
|
||||||
|
this.writeClient.on("reconnecting", () => {
|
||||||
|
logger.info("Regional Redis client reconnecting...");
|
||||||
|
this.isHealthy = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Regional Redis client initialized");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to initialize regional Redis client:", error);
|
||||||
|
this.isEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isRedisEnabled(): boolean {
|
||||||
|
return this.isEnabled && this.writeClient !== null && this.isHealthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHealthStatus() {
|
||||||
|
return { isEnabled: this.isEnabled, isHealthy: this.isHealthy };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
ttl?: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||||
|
try {
|
||||||
|
if (ttl) {
|
||||||
|
await this.writeClient.setex(key, ttl, value);
|
||||||
|
} else {
|
||||||
|
await this.writeClient.set(key, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis SET error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(key: string): Promise<string | null> {
|
||||||
|
if (!this.isRedisEnabled() || !this.readClient) return null;
|
||||||
|
try {
|
||||||
|
return await this.readClient.get(key);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis GET error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async del(key: string): Promise<boolean> {
|
||||||
|
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||||
|
try {
|
||||||
|
await this.writeClient.del(key);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis DEL error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async keys(pattern: string): Promise<string[]> {
|
||||||
|
if (!this.isRedisEnabled() || !this.readClient) return [];
|
||||||
|
try {
|
||||||
|
return await this.readClient.keys(pattern);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis KEYS error:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.writeClient) {
|
||||||
|
await this.writeClient.quit();
|
||||||
|
this.writeClient = null;
|
||||||
|
}
|
||||||
|
this.readClient = null;
|
||||||
|
logger.info("Regional Redis client disconnected");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error disconnecting regional Redis client:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regionalRedisManager = new RegionalRedisManager();
|
||||||
|
|||||||
@@ -124,15 +124,11 @@ function getWhere(data: Q) {
|
|||||||
data.clientId
|
data.clientId
|
||||||
? eq(connectionAuditLog.clientId, data.clientId)
|
? eq(connectionAuditLog.clientId, data.clientId)
|
||||||
: undefined,
|
: undefined,
|
||||||
data.siteId
|
data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined,
|
||||||
? eq(connectionAuditLog.siteId, data.siteId)
|
|
||||||
: undefined,
|
|
||||||
data.siteResourceId
|
data.siteResourceId
|
||||||
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
|
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
|
||||||
: undefined,
|
: undefined,
|
||||||
data.userId
|
data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined
|
||||||
? eq(connectionAuditLog.userId, data.userId)
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +140,7 @@ export function queryConnection(data: Q) {
|
|||||||
orgId: connectionAuditLog.orgId,
|
orgId: connectionAuditLog.orgId,
|
||||||
siteId: connectionAuditLog.siteId,
|
siteId: connectionAuditLog.siteId,
|
||||||
clientId: connectionAuditLog.clientId,
|
clientId: connectionAuditLog.clientId,
|
||||||
|
clientEndpoint: connectionAuditLog.clientEndpoint,
|
||||||
userId: connectionAuditLog.userId,
|
userId: connectionAuditLog.userId,
|
||||||
sourceAddr: connectionAuditLog.sourceAddr,
|
sourceAddr: connectionAuditLog.sourceAddr,
|
||||||
destAddr: connectionAuditLog.destAddr,
|
destAddr: connectionAuditLog.destAddr,
|
||||||
@@ -203,10 +200,7 @@ async function enrichWithDetails(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Fetch resource details from main database
|
// Fetch resource details from main database
|
||||||
const resourceMap = new Map<
|
const resourceMap = new Map<number, { name: string; niceId: string }>();
|
||||||
number,
|
|
||||||
{ name: string; niceId: string }
|
|
||||||
>();
|
|
||||||
if (siteResourceIds.length > 0) {
|
if (siteResourceIds.length > 0) {
|
||||||
const resourceDetails = await primaryDb
|
const resourceDetails = await primaryDb
|
||||||
.select({
|
.select({
|
||||||
@@ -268,10 +262,7 @@ async function enrichWithDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch user details from main database
|
// Fetch user details from main database
|
||||||
const userMap = new Map<
|
const userMap = new Map<string, { email: string | null }>();
|
||||||
string,
|
|
||||||
{ email: string | null }
|
|
||||||
>();
|
|
||||||
if (userIds.length > 0) {
|
if (userIds.length > 0) {
|
||||||
const userDetails = await primaryDb
|
const userDetails = await primaryDb
|
||||||
.select({
|
.select({
|
||||||
@@ -290,29 +281,25 @@ async function enrichWithDetails(
|
|||||||
return logs.map((log) => ({
|
return logs.map((log) => ({
|
||||||
...log,
|
...log,
|
||||||
resourceName: log.siteResourceId
|
resourceName: log.siteResourceId
|
||||||
? resourceMap.get(log.siteResourceId)?.name ?? null
|
? (resourceMap.get(log.siteResourceId)?.name ?? null)
|
||||||
: null,
|
: null,
|
||||||
resourceNiceId: log.siteResourceId
|
resourceNiceId: log.siteResourceId
|
||||||
? resourceMap.get(log.siteResourceId)?.niceId ?? null
|
? (resourceMap.get(log.siteResourceId)?.niceId ?? null)
|
||||||
: null,
|
|
||||||
siteName: log.siteId
|
|
||||||
? siteMap.get(log.siteId)?.name ?? null
|
|
||||||
: null,
|
: null,
|
||||||
|
siteName: log.siteId ? (siteMap.get(log.siteId)?.name ?? null) : null,
|
||||||
siteNiceId: log.siteId
|
siteNiceId: log.siteId
|
||||||
? siteMap.get(log.siteId)?.niceId ?? null
|
? (siteMap.get(log.siteId)?.niceId ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientName: log.clientId
|
clientName: log.clientId
|
||||||
? clientMap.get(log.clientId)?.name ?? null
|
? (clientMap.get(log.clientId)?.name ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientNiceId: log.clientId
|
clientNiceId: log.clientId
|
||||||
? clientMap.get(log.clientId)?.niceId ?? null
|
? (clientMap.get(log.clientId)?.niceId ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientType: log.clientId
|
clientType: log.clientId
|
||||||
? clientMap.get(log.clientId)?.type ?? null
|
? (clientMap.get(log.clientId)?.type ?? null)
|
||||||
: null,
|
: null,
|
||||||
userEmail: log.userId
|
userEmail: log.userId ? (userMap.get(log.userId)?.email ?? null) : null
|
||||||
? userMap.get(log.userId)?.email ?? null
|
|
||||||
: null
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,4 +508,4 @@ export async function queryConnectionAuditLogs(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export type ListEventStreamingDestinationsResponse = {
|
|||||||
type: string;
|
type: string;
|
||||||
config: string;
|
config: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
@@ -79,7 +81,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/event-streaming-destination",
|
path: "/org/{orgId}/event-streaming-destination",
|
||||||
description: "List all event streaming destinations for a specific organization.",
|
description:
|
||||||
|
"List all event streaming destinations for a specific organization.",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
query: querySchema,
|
query: querySchema,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from "@server/db";
|
import { clientSitesAssociationsCache, db } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { sites, Newt, clients, orgs } from "@server/db";
|
import { sites, Newt, clients, orgs } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
@@ -146,7 +146,11 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
|
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
|
||||||
const ipToClient = new Map<
|
const ipToClient = new Map<
|
||||||
string,
|
string,
|
||||||
{ clientId: number; userId: string | null }
|
{
|
||||||
|
clientId: number;
|
||||||
|
userId: string | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (cidrSuffix) {
|
if (cidrSuffix) {
|
||||||
@@ -172,9 +176,21 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
.select({
|
.select({
|
||||||
clientId: clients.clientId,
|
clientId: clients.clientId,
|
||||||
userId: clients.userId,
|
userId: clients.userId,
|
||||||
subnet: clients.subnet
|
subnet: clients.subnet,
|
||||||
|
clientEndpoint: clientSitesAssociationsCache.endpoint
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
|
.leftJoin(
|
||||||
|
// this should be one to one
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
clients.clientId,
|
||||||
|
clientSitesAssociationsCache.clientId
|
||||||
|
),
|
||||||
|
eq(clientSitesAssociationsCache.siteId, newt.siteId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(clients.orgId, orgId),
|
eq(clients.orgId, orgId),
|
||||||
@@ -189,7 +205,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
ipToClient.set(ip, {
|
ipToClient.set(ip, {
|
||||||
clientId: c.clientId,
|
clientId: c.clientId,
|
||||||
userId: c.userId
|
userId: c.userId,
|
||||||
|
clientEndpoint: c.clientEndpoint
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +251,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
orgId,
|
orgId,
|
||||||
siteId: newt.siteId,
|
siteId: newt.siteId,
|
||||||
clientId: clientInfo?.clientId ?? null,
|
clientId: clientInfo?.clientId ?? null,
|
||||||
|
clientEndpoint: clientInfo?.clientEndpoint ?? null,
|
||||||
userId: clientInfo?.userId ?? null,
|
userId: clientInfo?.userId ?? null,
|
||||||
sourceAddr: session.sourceAddr,
|
sourceAddr: session.sourceAddr,
|
||||||
destAddr: session.destAddr,
|
destAddr: session.destAddr,
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -98,15 +98,6 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser[0].isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the role of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleExists = await db
|
const roleExists = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
@@ -122,8 +113,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 +144,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";
|
||||||
@@ -98,11 +98,11 @@ export async function removeUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin === true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the roles of the owner of the organization"
|
"Cannot remove the administrator role from the organization owner"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
@@ -87,17 +87,8 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the roles of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgRoles = await db
|
const orgRoles = await db
|
||||||
.select({ roleId: roles.roleId })
|
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -115,6 +106,19 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingUser.isOwner) {
|
||||||
|
const hasAdminRole = orgRoles.some((r) => r.isAdmin === true);
|
||||||
|
if (!hasAdminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"The organization owner must retain an administrator role"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -142,11 +146,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,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export type QueryConnectionAuditLogResponse = {
|
|||||||
orgId: string | null;
|
orgId: string | null;
|
||||||
siteId: number | null;
|
siteId: number | null;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
sourceAddr: string;
|
sourceAddr: string;
|
||||||
destAddr: string;
|
destAddr: string;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -333,23 +333,16 @@ export async function validateOidcCallback(
|
|||||||
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
|
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
|
||||||
allOrgs = idpOrgs.map((o) => o.orgs);
|
allOrgs = idpOrgs.map((o) => o.orgs);
|
||||||
|
|
||||||
// for (const org of allOrgs) {
|
for (const org of allOrgs) {
|
||||||
// const subscribed = await isSubscribed(
|
const subscribed = await isSubscribed(
|
||||||
// org.orgId,
|
org.orgId,
|
||||||
// tierMatrix.autoProvisioning
|
tierMatrix.autoProvisioning
|
||||||
// );
|
);
|
||||||
// if (!subscribed) {
|
if (!subscribed) {
|
||||||
// // filter out the org
|
// filter out the org
|
||||||
// allOrgs = allOrgs.filter((o) => o.orgId !== org.orgId);
|
allOrgs = allOrgs.filter((o) => o.orgId !== org.orgId);
|
||||||
|
}
|
||||||
// // return next(
|
}
|
||||||
// // createHttpError(
|
|
||||||
// // HttpCode.FORBIDDEN,
|
|
||||||
// // "This organization's current plan does not support this feature."
|
|
||||||
// // )
|
|
||||||
// // );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
allOrgs = await db.select().from(orgs);
|
allOrgs = await db.select().from(orgs);
|
||||||
}
|
}
|
||||||
@@ -490,7 +483,14 @@ export async function validateOidcCallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(existingUser.userId);
|
calculateUserClientsForOrgs(existingUser.userId).catch(
|
||||||
|
(err) => {
|
||||||
|
logger.error(
|
||||||
|
"Error calculating user clients after removing all orgs for user with no valid IdP mappings",
|
||||||
|
{ error: err }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -512,10 +512,9 @@ export async function validateOidcCallback(
|
|||||||
|
|
||||||
const orgUserCounts: { orgId: string; userCount: number }[] = [];
|
const orgUserCounts: { orgId: string; userCount: number }[] = [];
|
||||||
|
|
||||||
|
let userId = existingUser?.userId;
|
||||||
// sync the user with the orgs and roles
|
// sync the user with the orgs and roles
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
let userId = existingUser?.userId;
|
|
||||||
|
|
||||||
// create user if not exists
|
// create user if not exists
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
userId = generateId(15);
|
userId = generateId(15);
|
||||||
@@ -645,8 +644,15 @@ export async function validateOidcCallback(
|
|||||||
userCount: userCount.length
|
userCount: userCount.length
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.transaction(async (trx) => {
|
||||||
await calculateUserClientsForOrgs(userId!, trx);
|
await calculateUserClientsForOrgs(userId!, trx);
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.error(
|
||||||
|
"Error calculating user clients after syncing orgs and roles for OIDC user",
|
||||||
|
{ error: err }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const orgCount of orgUserCounts) {
|
for (const orgCount of orgUserCounts) {
|
||||||
|
|||||||
@@ -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,16 +81,19 @@ 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,
|
secretHash,
|
||||||
secretHash,
|
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,23 +67,28 @@ 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) {
|
|
||||||
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-site associations after deleting OLM ${olmId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
OlmErrorCodes.TERMINATED_DELETED,
|
||||||
|
olmId
|
||||||
|
).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,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -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(
|
||||||
olmId: olm.olmId,
|
"[handleOlmRegisterMessage] Handling fingerprint insertion for olm register...",
|
||||||
fingerprint,
|
{
|
||||||
postures
|
olmId: olm.olmId,
|
||||||
});
|
fingerprint,
|
||||||
|
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,12 +309,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||||
client,
|
client,
|
||||||
publicKey,
|
publicKey,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,16 +75,14 @@ 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()])
|
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
.safeParse(data.destination).success;
|
||||||
.safeParse(data.destination).success;
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -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") {
|
||||||
return true;
|
// we have to have a domainId defined
|
||||||
},
|
if (!data.domainId) {
|
||||||
{
|
return false;
|
||||||
message:
|
}
|
||||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
} else if (data.mode === "cidr") {
|
||||||
}
|
|
||||||
)
|
|
||||||
.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!,
|
primaryDb
|
||||||
trx
|
).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,
|
primaryDb
|
||||||
trx
|
).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";
|
||||||
@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin !== true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the role of the owner of the organization"
|
"The organization owner must retain an administrator role"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -292,20 +298,30 @@ export async function createOrgUser(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await assignUserToOrg(
|
await assignUserToOrg(
|
||||||
org,
|
org,
|
||||||
{
|
{
|
||||||
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,10 +47,7 @@ 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(
|
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||||
eq(userOrgRoles.userId, userId),
|
|
||||||
eq(userOrgRoles.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||||
@@ -61,7 +58,8 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
roleIds: roleRows.map((r) => r.roleId),
|
roleIds: roleRows.map((r) => r.roleId),
|
||||||
roles: roleRows.map((r) => ({
|
roles: roleRows.map((r) => ({
|
||||||
roleId: r.roleId,
|
roleId: r.roleId,
|
||||||
name: r.roleName ?? ""
|
name: r.roleName ?? "",
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import m15 from "./scriptsPg/1.16.0";
|
|||||||
import m16 from "./scriptsPg/1.17.0";
|
import m16 from "./scriptsPg/1.17.0";
|
||||||
import m17 from "./scriptsPg/1.18.0";
|
import m17 from "./scriptsPg/1.18.0";
|
||||||
import m18 from "./scriptsPg/1.18.3";
|
import m18 from "./scriptsPg/1.18.3";
|
||||||
|
import m19 from "./scriptsPg/1.18.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -47,7 +48,8 @@ const migrations = [
|
|||||||
{ version: "1.16.0", run: m15 },
|
{ version: "1.16.0", run: m15 },
|
||||||
{ version: "1.17.0", run: m16 },
|
{ version: "1.17.0", run: m16 },
|
||||||
{ version: "1.18.0", run: m17 },
|
{ version: "1.18.0", run: m17 },
|
||||||
{ version: "1.18.3", run: m18 }
|
{ version: "1.18.3", run: m18 },
|
||||||
|
{ version: "1.18.4", run: m19 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import m36 from "./scriptsSqlite/1.16.0";
|
|||||||
import m37 from "./scriptsSqlite/1.17.0";
|
import m37 from "./scriptsSqlite/1.17.0";
|
||||||
import m38 from "./scriptsSqlite/1.18.0";
|
import m38 from "./scriptsSqlite/1.18.0";
|
||||||
import m39 from "./scriptsSqlite/1.18.3";
|
import m39 from "./scriptsSqlite/1.18.3";
|
||||||
|
import m40 from "./scriptsSqlite/1.18.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -81,7 +82,8 @@ const migrations = [
|
|||||||
{ version: "1.16.0", run: m36 },
|
{ version: "1.16.0", run: m36 },
|
||||||
{ version: "1.17.0", run: m37 },
|
{ version: "1.17.0", run: m37 },
|
||||||
{ version: "1.18.0", run: m38 },
|
{ version: "1.18.0", run: m38 },
|
||||||
{ version: "1.18.3", run: m39 }
|
{ version: "1.18.3", run: m39 },
|
||||||
|
{ version: "1.18.4", run: m40 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
34
server/setup/scriptsPg/1.18.4.ts
Normal file
34
server/setup/scriptsPg/1.18.4.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
const version = "1.18.4";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "connectionAuditLog" ADD COLUMN "clientEndpoint" text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "eventStreamingDestinations" ADD COLUMN "lastError" text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "eventStreamingDestinations" ADD COLUMN "lastErrorAt" bigint;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
43
server/setup/scriptsSqlite/1.18.4.ts
Normal file
43
server/setup/scriptsSqlite/1.18.4.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.18.4";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'connectionAuditLog' ADD 'clientEndpoint' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'eventStreamingDestinations' ADD 'lastError' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'eventStreamingDestinations' ADD 'lastErrorAt' integer;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -175,26 +175,6 @@ export default function GeneralPage() {
|
|||||||
}, [variant]);
|
}, [variant]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchRoles() {
|
|
||||||
const res = await api
|
|
||||||
.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`)
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("accessRoleErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("accessRoleErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
setRoles(res.data.data.roles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadIdp = async (
|
const loadIdp = async (
|
||||||
availableRoles: { roleId: number; name: string }[]
|
availableRoles: { roleId: number; name: string }[]
|
||||||
) => {
|
) => {
|
||||||
@@ -520,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);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
||||||
|
import {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel
|
||||||
FormMessage
|
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AxiosResponse } from "axios";
|
import { build } from "@server/build";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
|
||||||
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionForm,
|
|
||||||
SettingsSectionFooter
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
|
||||||
import { createApiClient } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
|
||||||
import { UserType } from "@server/types/UserTypes";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
const accessControlsFormSchema = z.object({
|
const accessControlsFormSchema = z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
@@ -46,25 +44,21 @@ const accessControlsFormSchema = z.object({
|
|||||||
roles: z.array(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
text: z.string()
|
text: z.string(),
|
||||||
|
isAdmin: z.boolean().optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function AccessControlsPage() {
|
export default function AccessControlsPage() {
|
||||||
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
||||||
|
const { user: sessionUser } = useUserContext();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
|
||||||
const [activeRoleTagIndex, setActiveRoleTagIndex] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
||||||
@@ -82,7 +76,8 @@ export default function AccessControlsPage() {
|
|||||||
autoProvisioned: user.autoProvisioned || false,
|
autoProvisioned: user.autoProvisioned || false,
|
||||||
roles: (user.roles ?? []).map((r) => ({
|
roles: (user.roles ?? []).map((r) => ({
|
||||||
id: r.roleId.toString(),
|
id: r.roleId.toString(),
|
||||||
text: r.name
|
text: r.name,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -94,47 +89,25 @@ export default function AccessControlsPage() {
|
|||||||
"roles",
|
"roles",
|
||||||
(user.roles ?? []).map((r) => ({
|
(user.roles ?? []).map((r) => ({
|
||||||
id: r.roleId.toString(),
|
id: r.roleId.toString(),
|
||||||
text: r.name
|
text: r.name,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [user.userId, currentRoleIds.join(",")]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchRoles() {
|
|
||||||
const res = await api
|
|
||||||
.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`)
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("accessRoleErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("accessRoleErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
setRoles(res.data.data.roles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchRoles();
|
|
||||||
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||||
}, []);
|
}, [user.userId, user.autoProvisioned, currentRoleIds.join(",")]);
|
||||||
|
|
||||||
const allRoleOptions = roles.map((role) => ({
|
|
||||||
id: role.roleId.toString(),
|
|
||||||
text: role.name
|
|
||||||
}));
|
|
||||||
|
|
||||||
const paywallMessage =
|
const paywallMessage =
|
||||||
build === "saas"
|
build === "saas"
|
||||||
? t("singleRolePerUserPlanNotice")
|
? t("singleRolePerUserPlanNotice")
|
||||||
: t("singleRolePerUserEditionNotice");
|
: t("singleRolePerUserEditionNotice");
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof accessControlsFormSchema>) {
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [confirmRemoveOwnAdminOpen, setConfirmRemoveOwnAdminOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
async function executeSave() {
|
||||||
|
const values = form.getValues();
|
||||||
|
|
||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
@@ -144,7 +117,7 @@ export default function AccessControlsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
const updateRoleRequest = supportsMultipleRolesPerUser
|
const updateRoleRequest = supportsMultipleRolesPerUser
|
||||||
@@ -164,7 +137,8 @@ export default function AccessControlsPage() {
|
|||||||
roleIds,
|
roleIds,
|
||||||
roles: values.roles.map((r) => ({
|
roles: values.roles.map((r) => ({
|
||||||
roleId: parseInt(r.id, 10),
|
roleId: parseInt(r.id, 10),
|
||||||
name: r.text
|
name: r.text,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
})),
|
})),
|
||||||
autoProvisioned: values.autoProvisioned
|
autoProvisioned: values.autoProvisioned
|
||||||
});
|
});
|
||||||
@@ -183,12 +157,61 @@ export default function AccessControlsPage() {
|
|||||||
t("accessRoleErrorAddDescription")
|
t("accessRoleErrorAddDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
}
|
||||||
|
|
||||||
|
async function handleAccessControlsSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const isValid = await form.trigger();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const values = form.getValues();
|
||||||
|
|
||||||
|
if (values.roles.length === 0) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleErrorAdd"),
|
||||||
|
description: t("accessRoleSelectPlease")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const willHaveAdminRole = values.roles.some(
|
||||||
|
(r) => r.isAdmin === true
|
||||||
|
);
|
||||||
|
|
||||||
|
const isRemovingOwnAdmin =
|
||||||
|
sessionUser.userId === user.userId &&
|
||||||
|
user.isAdmin &&
|
||||||
|
!willHaveAdminRole;
|
||||||
|
|
||||||
|
if (isRemovingOwnAdmin) {
|
||||||
|
setConfirmRemoveOwnAdminOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={confirmRemoveOwnAdminOpen}
|
||||||
|
setOpen={setConfirmRemoveOwnAdminOpen}
|
||||||
|
title={t("removeOwnAdminRoleConfirmTitle")}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("removeOwnAdminRoleConfirmDescription")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("removeOwnAdminRoleConfirmButton")}
|
||||||
|
string={t("removeOwnAdminRoleConfirmPhrase")}
|
||||||
|
onConfirm={executeSave}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
@@ -203,7 +226,7 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
@@ -226,9 +249,7 @@ export default function AccessControlsPage() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={form}
|
form={form}
|
||||||
name="roles"
|
name="roles"
|
||||||
label={t("roles")}
|
orgId={orgId as string}
|
||||||
placeholder={t("accessRoleSelect2")}
|
|
||||||
allRoleOptions={allRoleOptions}
|
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -236,9 +257,6 @@ export default function AccessControlsPage() {
|
|||||||
showMultiRolePaywallMessage
|
showMultiRolePaywallMessage
|
||||||
}
|
}
|
||||||
paywallMessage={paywallMessage}
|
paywallMessage={paywallMessage}
|
||||||
loading={loading}
|
|
||||||
activeTagIndex={activeRoleTagIndex}
|
|
||||||
setActiveTagIndex={setActiveRoleTagIndex}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{user.idpAutoProvision && (
|
{user.idpAutoProvision && (
|
||||||
@@ -277,8 +295,8 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={loading}
|
loading={isSaving}
|
||||||
disabled={loading}
|
disabled={isSaving}
|
||||||
form="access-controls-form"
|
form="access-controls-form"
|
||||||
>
|
>
|
||||||
{t("accessControlsSubmit")}
|
{t("accessControlsSubmit")}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
|
|||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useActionState, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -91,7 +91,7 @@ export default function Page() {
|
|||||||
"internal"
|
"internal"
|
||||||
);
|
);
|
||||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
const [idps, setIdps] = useState<IdpOption[]>([]);
|
const [idps, setIdps] = useState<IdpOption[]>([]);
|
||||||
@@ -311,10 +311,29 @@ export default function Page() {
|
|||||||
setUserOptions(options);
|
setUserOptions(options);
|
||||||
}, [idps, t]);
|
}, [idps, t]);
|
||||||
|
|
||||||
async function onSubmitInternal(
|
const [, submitInternalAction, isSubmittingInternal] = useActionState(
|
||||||
values: z.infer<typeof internalFormSchema>
|
onSubmitInternal,
|
||||||
) {
|
null
|
||||||
setLoading(true);
|
);
|
||||||
|
const [, submitGoogleAzureAction, isSubmittingGoogleAzure] = useActionState(
|
||||||
|
onSubmitGoogleAzure,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [, submitGenericOidcAction, isSubmittingGenericOidc] = useActionState(
|
||||||
|
onSubmitGenericOidc,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const loading =
|
||||||
|
isSubmittingInternal ||
|
||||||
|
isSubmittingGoogleAzure ||
|
||||||
|
isSubmittingGenericOidc;
|
||||||
|
|
||||||
|
async function onSubmitInternal() {
|
||||||
|
const isValid = await internalForm.trigger();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const values = internalForm.getValues();
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
@@ -357,25 +376,24 @@ export default function Page() {
|
|||||||
|
|
||||||
setExpiresInDays(parseInt(values.validForHours) / 24);
|
setExpiresInDays(parseInt(values.validForHours) / 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGoogleAzure(
|
async function onSubmitGoogleAzure() {
|
||||||
values: z.infer<typeof googleAzureFormSchema>
|
const isValid = await googleAzureForm.trigger();
|
||||||
) {
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const values = googleAzureForm.getValues();
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
.put(`/org/${orgId}/user`, {
|
.put(`/org/${orgId}/user`, {
|
||||||
username: values.email, // Use email as username for Google/Azure
|
username: values.email,
|
||||||
email: values.email || undefined,
|
email: values.email || undefined,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
@@ -401,20 +419,19 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGenericOidc(
|
async function onSubmitGenericOidc() {
|
||||||
values: z.infer<typeof genericOidcFormSchema>
|
const isValid = await genericOidcForm.trigger();
|
||||||
) {
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const values = genericOidcForm.getValues();
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -445,8 +462,6 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -513,9 +528,9 @@ export default function Page() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...internalForm}>
|
<Form {...internalForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={internalForm.handleSubmit(
|
action={
|
||||||
onSubmitInternal
|
submitInternalAction
|
||||||
)}
|
}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -595,13 +610,7 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={internalForm}
|
form={internalForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
label={t("roles")}
|
orgId={orgId as string}
|
||||||
placeholder={t(
|
|
||||||
"accessRoleSelect2"
|
|
||||||
)}
|
|
||||||
allRoleOptions={
|
|
||||||
allRoleOptions
|
|
||||||
}
|
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -611,13 +620,6 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
loading={loading}
|
|
||||||
activeTagIndex={
|
|
||||||
activeInviteRoleTagIndex
|
|
||||||
}
|
|
||||||
setActiveTagIndex={
|
|
||||||
setActiveInviteRoleTagIndex
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{env.email.emailEnabled && (
|
{env.email.emailEnabled && (
|
||||||
@@ -712,9 +714,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...googleAzureForm}>
|
<Form {...googleAzureForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={googleAzureForm.handleSubmit(
|
action={
|
||||||
onSubmitGoogleAzure
|
submitGoogleAzureAction
|
||||||
)}
|
}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -763,13 +765,7 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={googleAzureForm}
|
form={googleAzureForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
label={t("roles")}
|
orgId={orgId as string}
|
||||||
placeholder={t(
|
|
||||||
"accessRoleSelect2"
|
|
||||||
)}
|
|
||||||
allRoleOptions={
|
|
||||||
allRoleOptions
|
|
||||||
}
|
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -779,13 +775,6 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
loading={loading}
|
|
||||||
activeTagIndex={
|
|
||||||
activeOidcRoleTagIndex
|
|
||||||
}
|
|
||||||
setActiveTagIndex={
|
|
||||||
setActiveOidcRoleTagIndex
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -808,9 +797,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...genericOidcForm}>
|
<Form {...genericOidcForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={genericOidcForm.handleSubmit(
|
action={
|
||||||
onSubmitGenericOidc
|
submitGenericOidcAction
|
||||||
)}
|
}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -888,13 +877,7 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={genericOidcForm}
|
form={genericOidcForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
label={t("roles")}
|
orgId={orgId as string}
|
||||||
placeholder={t(
|
|
||||||
"accessRoleSelect2"
|
|
||||||
)}
|
|
||||||
allRoleOptions={
|
|
||||||
allRoleOptions
|
|
||||||
}
|
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -904,13 +887,6 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
loading={loading}
|
|
||||||
activeTagIndex={
|
|
||||||
activeOidcRoleTagIndex
|
|
||||||
}
|
|
||||||
setActiveTagIndex={
|
|
||||||
setActiveOidcRoleTagIndex
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -645,6 +645,12 @@ export default function ConnectionLogsPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
|
<div>
|
||||||
|
<strong>Client Endpoint:</strong>{" "}
|
||||||
|
<span className="font-mono">
|
||||||
|
{row.clientEndpoint ?? "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Site:</strong> {row.siteName ?? "-"}
|
<strong>Site:</strong> {row.siteName ?? "-"}
|
||||||
{row.siteNiceId && (
|
{row.siteNiceId && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
@@ -22,7 +22,18 @@ import {
|
|||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { Globe, MoreHorizontal, Plus } from "lucide-react";
|
import {
|
||||||
|
Globe,
|
||||||
|
MoreHorizontal,
|
||||||
|
Plus,
|
||||||
|
AlertCircle,
|
||||||
|
ChevronDown
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@@ -38,7 +49,10 @@ import {
|
|||||||
HttpDestinationCredenza,
|
HttpDestinationCredenza,
|
||||||
parseHttpConfig
|
parseHttpConfig
|
||||||
} from "@app/components/HttpDestinationCredenza";
|
} from "@app/components/HttpDestinationCredenza";
|
||||||
import { S3DestinationCredenza } from "@app/components/S3DestinationCredenza";
|
import {
|
||||||
|
S3DestinationCredenza,
|
||||||
|
parseS3Config
|
||||||
|
} from "@app/components/S3DestinationCredenza";
|
||||||
import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza";
|
import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -64,6 +78,42 @@ interface DestinationCardProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDestinationDisplay(destination: Destination): {
|
||||||
|
name: string;
|
||||||
|
typeLabel: string;
|
||||||
|
detail: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
} {
|
||||||
|
if (destination.type === "s3") {
|
||||||
|
const cfg = parseS3Config(destination.config);
|
||||||
|
const detail = cfg.bucket
|
||||||
|
? `s3://${cfg.bucket}${cfg.prefix ? `/${cfg.prefix.replace(/^\/+/, "")}` : ""}`
|
||||||
|
: "";
|
||||||
|
return {
|
||||||
|
name: cfg.name,
|
||||||
|
typeLabel: "Amazon S3",
|
||||||
|
detail,
|
||||||
|
icon: (
|
||||||
|
<Image
|
||||||
|
src="/third-party/s3.png"
|
||||||
|
alt="Amazon S3"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded-sm"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Default: HTTP
|
||||||
|
const cfg = parseHttpConfig(destination.config);
|
||||||
|
return {
|
||||||
|
name: cfg.name,
|
||||||
|
typeLabel: "HTTP",
|
||||||
|
detail: cfg.url,
|
||||||
|
icon: <Globe className="h-3.5 w-3.5 text-black" />
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function DestinationCard({
|
function DestinationCard({
|
||||||
destination,
|
destination,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -73,25 +123,25 @@ function DestinationCard({
|
|||||||
disabled = false
|
disabled = false
|
||||||
}: DestinationCardProps) {
|
}: DestinationCardProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const cfg = parseHttpConfig(destination.config);
|
const { name, typeLabel, detail, icon } =
|
||||||
|
getDestinationDisplay(destination);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col rounded-lg border bg-card text-card-foreground p-5 gap-3">
|
<div className="relative flex flex-col rounded-lg border bg-card text-card-foreground p-5 gap-3">
|
||||||
{/* Top row: icon + name/type + toggle */}
|
{/* Top row: icon + name/type + toggle */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{/* Squirkle icon: gray outer → white inner → black globe */}
|
|
||||||
<div className="shrink-0 flex items-center justify-center w-10 h-10 rounded-2xl bg-muted">
|
<div className="shrink-0 flex items-center justify-center w-10 h-10 rounded-2xl bg-muted">
|
||||||
<div className="flex items-center justify-center w-6 h-6 rounded-xl bg-white shadow-sm">
|
<div className="flex items-center justify-center w-6 h-6 rounded-xl bg-white shadow-sm">
|
||||||
<Globe className="h-3.5 w-3.5 text-black" />
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-semibold text-sm leading-tight truncate">
|
<p className="font-semibold text-sm leading-tight truncate">
|
||||||
{cfg.name || t("streamingUnnamedDestination")}
|
{name || t("streamingUnnamedDestination")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||||
HTTP
|
{typeLabel}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,15 +155,40 @@ function DestinationCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL preview */}
|
{/* Detail preview (URL for HTTP, s3:// path for S3) */}
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{cfg.url || (
|
{detail || (
|
||||||
<span className="italic">
|
<span className="italic">
|
||||||
{t("streamingNoUrlConfigured")}
|
{t("streamingNoUrlConfigured")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Error indicator */}
|
||||||
|
{destination.lastError && (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 text-left cursor-pointer rounded px-0 hover:opacity-75 transition-opacity"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{t("streamingLastSyncError")}
|
||||||
|
</p>
|
||||||
|
<ChevronDown className="h-3 w-3 text-destructive shrink-0 ml-auto" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
className="w-80 text-xs break-words"
|
||||||
|
>
|
||||||
|
{destination.lastError}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer: edit button + three-dots menu */}
|
{/* Footer: edit button + three-dots menu */}
|
||||||
<div className="mt-auto pt-5 flex gap-2">
|
<div className="mt-auto pt-5 flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -485,7 +560,7 @@ export default function StreamingDestinationsPage() {
|
|||||||
if (!v) setDeleteTarget(null);
|
if (!v) setDeleteTarget(null);
|
||||||
}}
|
}}
|
||||||
string={
|
string={
|
||||||
parseHttpConfig(deleteTarget.config).name ||
|
getDestinationDisplay(deleteTarget).name ||
|
||||||
t("streamingDeleteDialogThisDestination")
|
t("streamingDeleteDialogThisDestination")
|
||||||
}
|
}
|
||||||
title={t("streamingDeleteTitle")}
|
title={t("streamingDeleteTitle")}
|
||||||
@@ -493,7 +568,7 @@ export default function StreamingDestinationsPage() {
|
|||||||
<p>
|
<p>
|
||||||
{t("streamingDeleteDialogAreYouSure")}{" "}
|
{t("streamingDeleteDialogAreYouSure")}{" "}
|
||||||
<span>
|
<span>
|
||||||
{parseHttpConfig(deleteTarget.config).name ||
|
{getDestinationDisplay(deleteTarget).name ||
|
||||||
t("streamingDeleteDialogThisDestination")}
|
t("streamingDeleteDialogThisDestination")}
|
||||||
</span>
|
</span>
|
||||||
{t("streamingDeleteDialogPermanentlyRemoved")}
|
{t("streamingDeleteDialogPermanentlyRemoved")}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { RolesSelector } from "@app/components/roles-selector";
|
||||||
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
||||||
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
import { UsersSelector } from "@app/components/users-selector";
|
||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
@@ -180,13 +182,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
return [];
|
return [];
|
||||||
}, [orgIdps]);
|
}, [orgIdps]);
|
||||||
|
|
||||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false);
|
const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -497,46 +492,27 @@ export default function ResourceAuthenticationPage() {
|
|||||||
{t("roles")}
|
{t("roles")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<RolesSelector
|
||||||
{...field}
|
selectedRoles={
|
||||||
activeTagIndex={
|
field.value ??
|
||||||
activeRolesTagIndex
|
[]
|
||||||
}
|
}
|
||||||
setActiveTagIndex={
|
restrictAdminRole
|
||||||
setActiveRolesTagIndex
|
orgId={
|
||||||
|
org.org
|
||||||
|
.orgId
|
||||||
}
|
}
|
||||||
placeholder={t(
|
onSelectRoles={(
|
||||||
"accessRoleSelect2"
|
newUsers
|
||||||
)}
|
|
||||||
size="sm"
|
|
||||||
tags={
|
|
||||||
usersRolesForm.getValues()
|
|
||||||
.roles
|
|
||||||
}
|
|
||||||
setTags={(
|
|
||||||
newRoles
|
|
||||||
) => {
|
) => {
|
||||||
usersRolesForm.setValue(
|
usersRolesForm.setValue(
|
||||||
"roles",
|
"roles",
|
||||||
newRoles as [
|
newUsers as [
|
||||||
Tag,
|
Tag,
|
||||||
...Tag[]
|
...Tag[]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
enableAutocomplete={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
autocompleteOptions={
|
|
||||||
allRoles
|
|
||||||
}
|
|
||||||
allowDuplicates={
|
|
||||||
false
|
|
||||||
}
|
|
||||||
restrictTagsToAutocompleteOptions={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
sortTags={true}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -557,23 +533,16 @@ export default function ResourceAuthenticationPage() {
|
|||||||
{t("users")}
|
{t("users")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<UsersSelector
|
||||||
{...field}
|
selectedUsers={
|
||||||
activeTagIndex={
|
field.value ??
|
||||||
activeUsersTagIndex
|
[]
|
||||||
}
|
}
|
||||||
setActiveTagIndex={
|
orgId={
|
||||||
setActiveUsersTagIndex
|
org.org
|
||||||
|
.orgId
|
||||||
}
|
}
|
||||||
placeholder={t(
|
onSelectUsers={(
|
||||||
"accessUserSelect"
|
|
||||||
)}
|
|
||||||
tags={
|
|
||||||
usersRolesForm.getValues()
|
|
||||||
.users
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
setTags={(
|
|
||||||
newUsers
|
newUsers
|
||||||
) => {
|
) => {
|
||||||
usersRolesForm.setValue(
|
usersRolesForm.setValue(
|
||||||
@@ -584,19 +553,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
enableAutocomplete={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
autocompleteOptions={
|
|
||||||
allUsers
|
|
||||||
}
|
|
||||||
allowDuplicates={
|
|
||||||
false
|
|
||||||
}
|
|
||||||
restrictTagsToAutocompleteOptions={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
sortTags={true}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
|
ExternalLink,
|
||||||
Info,
|
Info,
|
||||||
Plus,
|
Plus,
|
||||||
Settings
|
Settings
|
||||||
@@ -961,13 +962,18 @@ function ProxyResourceTargetsForm({
|
|||||||
{build === "saas" &&
|
{build === "saas" &&
|
||||||
targets.length > 1 &&
|
targets.length > 1 &&
|
||||||
new Set(targets.map((t) => t.siteId)).size > 1 && (
|
new Set(targets.map((t) => t.siteId)).size > 1 && (
|
||||||
<p className="text-sm text-muted-foreground mt-3 flex items-start gap-1.5">
|
<p className="text-sm text-muted-foreground mt-3">
|
||||||
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
|
{t("proxyMultiSiteRoundRobinNodeHelp")}{" "}
|
||||||
<span>
|
<a
|
||||||
Round robin routing will not work between
|
href="https://docs.pangolin.net/manage/resources/public/targets#distributing-sites-load-across-servers"
|
||||||
sites that are not connected to the same
|
target="_blank"
|
||||||
node, but failover will work.
|
rel="noopener noreferrer"
|
||||||
</span>
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t("learnMore")}
|
||||||
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ import { AxiosResponse } from "axios";
|
|||||||
import {
|
import {
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
|
ExternalLink,
|
||||||
Info,
|
Info,
|
||||||
InfoIcon,
|
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
SquareArrowOutUpRight
|
SquareArrowOutUpRight
|
||||||
@@ -1425,16 +1425,22 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{build === "enterprise" &&
|
{build === "saas" &&
|
||||||
targets.length > 1 &&
|
targets.length > 1 &&
|
||||||
new Set(targets.map((t) => t.siteId)).size > 1 && (
|
new Set(targets.map((t) => t.siteId)).size >
|
||||||
<p className="text-sm text-muted-foreground mt-3 flex items-start gap-1.5">
|
1 && (
|
||||||
<InfoIcon className="h-4 w-4 shrink-0 mt-0.5" />
|
<p className="text-sm text-muted-foreground mt-3">
|
||||||
<span>
|
{t("proxyMultiSiteRoundRobinNodeHelp")}{" "}
|
||||||
Round robin routing will not work between
|
<a
|
||||||
sites that are not connected to the same
|
href="https://docs.pangolin.net/manage/resources/public/targets#distributing-sites-load-across-servers"
|
||||||
node, but failover will work.
|
target="_blank"
|
||||||
</span>
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t("learnMore")}
|
||||||
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export default async function ProxyResourcesPage(
|
|||||||
pagination = responseData.pagination;
|
pagination = responseData.pagination;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
const siteIdParam = parsePositiveInt(searchParams.get("siteId") ?? undefined);
|
const siteIdParam = parsePositiveInt(
|
||||||
|
searchParams.get("siteId") ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
let initialFilterSite: {
|
let initialFilterSite: {
|
||||||
siteId: number;
|
siteId: number;
|
||||||
@@ -122,6 +124,7 @@ export default async function ProxyResourcesPage(
|
|||||||
domainId: resource.domainId || undefined,
|
domainId: resource.domainId || undefined,
|
||||||
fullDomain: resource.fullDomain ?? null,
|
fullDomain: resource.fullDomain ?? null,
|
||||||
ssl: resource.ssl,
|
ssl: resource.ssl,
|
||||||
|
wildcard: resource.wildcard,
|
||||||
targets: resource.targets?.map((target) => ({
|
targets: resource.targets?.map((target) => ({
|
||||||
targetId: target.targetId,
|
targetId: target.targetId,
|
||||||
ip: target.ip,
|
ip: target.ip,
|
||||||
|
|||||||
@@ -681,6 +681,9 @@ export default function PoliciesPage() {
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
name: "orgMapping"
|
name: "orgMapping"
|
||||||
}}
|
}}
|
||||||
|
orgId={
|
||||||
|
editingPolicy?.orgId || policyFormOrgId
|
||||||
|
}
|
||||||
roleMappingFieldIdPrefix="admin-idp-policy-role"
|
roleMappingFieldIdPrefix="admin-idp-policy-role"
|
||||||
roleMappingMode={policyRoleMappingMode}
|
roleMappingMode={policyRoleMappingMode}
|
||||||
onRoleMappingModeChange={
|
onRoleMappingModeChange={
|
||||||
|
|||||||
@@ -212,16 +212,22 @@ export const orgNavSections = (
|
|||||||
title: "sidebarManagement",
|
title: "sidebarManagement",
|
||||||
icon: <Building2 className="size-4 flex-none" />,
|
icon: <Building2 className="size-4 flex-none" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
...(!env?.flags.disableEnterpriseFeatures
|
||||||
title: "sidebarAlerting",
|
? [
|
||||||
href: "/{orgId}/settings/alerting",
|
{
|
||||||
icon: <BellRing className="size-4 flex-none" />
|
title: "sidebarAlerting",
|
||||||
},
|
href: "/{orgId}/settings/alerting",
|
||||||
{
|
icon: (
|
||||||
title: "sidebarProvisioning",
|
<BellRing className="size-4 flex-none" />
|
||||||
href: "/{orgId}/settings/provisioning",
|
)
|
||||||
icon: <Boxes className="size-4 flex-none" />
|
},
|
||||||
},
|
{
|
||||||
|
title: "sidebarProvisioning",
|
||||||
|
href: "/{orgId}/settings/provisioning",
|
||||||
|
icon: <Boxes className="size-4 flex-none" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
title: "sidebarBluePrints",
|
title: "sidebarBluePrints",
|
||||||
href: "/{orgId}/settings/blueprints",
|
href: "/{orgId}/settings/blueprints",
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ export default function AlertingRulesTable({
|
|||||||
}: AlertingRulesTableProps) {
|
}: AlertingRulesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const envContext = useEnvContext();
|
||||||
|
const api = createApiClient(envContext);
|
||||||
|
const { env } = envContext;
|
||||||
const [isRefreshing, startRefresh] = useTransition();
|
const [isRefreshing, startRefresh] = useTransition();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||||
@@ -426,9 +428,15 @@ export default function AlertingRulesTable({
|
|||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
manualFiltering
|
manualFiltering
|
||||||
manualSorting
|
manualSorting
|
||||||
onAdd={() => {
|
onAdd={
|
||||||
router.push(`/${orgId}/settings/alerting/create`);
|
!env.flags.disableEnterpriseFeatures
|
||||||
}}
|
? () => {
|
||||||
|
router.push(
|
||||||
|
`/${orgId}/settings/alerting/create`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onRefresh={refreshList}
|
onRefresh={refreshList}
|
||||||
isRefreshing={isRefreshing || isFiltering}
|
isRefreshing={isRefreshing || isFiltering}
|
||||||
addButtonText={t("alertingAddRule")}
|
addButtonText={t("alertingAddRule")}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type AutoProvisionConfigWidgetProps = {
|
|||||||
roleMappingFieldIdPrefix?: string;
|
roleMappingFieldIdPrefix?: string;
|
||||||
showFreeformRoleNamesHint?: boolean;
|
showFreeformRoleNamesHint?: boolean;
|
||||||
autoProvisionSwitchId?: string;
|
autoProvisionSwitchId?: string;
|
||||||
|
orgId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AutoProvisionConfigWidget({
|
export default function AutoProvisionConfigWidget({
|
||||||
@@ -67,7 +68,8 @@ export default function AutoProvisionConfigWidget({
|
|||||||
showAutoProvisionSwitch = true,
|
showAutoProvisionSwitch = true,
|
||||||
roleMappingFieldIdPrefix = "org-idp-auto-provision",
|
roleMappingFieldIdPrefix = "org-idp-auto-provision",
|
||||||
showFreeformRoleNamesHint = false,
|
showFreeformRoleNamesHint = false,
|
||||||
autoProvisionSwitchId = "auto-provision-toggle"
|
autoProvisionSwitchId = "auto-provision-toggle",
|
||||||
|
orgId
|
||||||
}: AutoProvisionConfigWidgetProps) {
|
}: AutoProvisionConfigWidgetProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
@@ -106,6 +108,7 @@ export default function AutoProvisionConfigWidget({
|
|||||||
showFreeformRoleNamesHint={
|
showFreeformRoleNamesHint={
|
||||||
showFreeformRoleNamesHint
|
showFreeformRoleNamesHint
|
||||||
}
|
}
|
||||||
|
orgId={orgId}
|
||||||
roleMappingMode={roleMappingMode}
|
roleMappingMode={roleMappingMode}
|
||||||
onRoleMappingModeChange={onRoleMappingModeChange}
|
onRoleMappingModeChange={onRoleMappingModeChange}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
return (
|
return (
|
||||||
<CredenzaContent
|
<CredenzaContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 max-h-[100dvh] flex-col overflow-hidden md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import DomainCertForm from "@app/components/DomainCertForm";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Lock } from "lucide-react";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
|
||||||
interface DomainPageClientProps {
|
interface DomainPageClientProps {
|
||||||
initialDomain: GetDomainResponse;
|
initialDomain: GetDomainResponse;
|
||||||
@@ -49,7 +51,22 @@ export default function DomainPageClient({
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={domain.baseDomain}
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{domain.baseDomain}
|
||||||
|
{domain.configManaged && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1 text-sm font-normal"
|
||||||
|
>
|
||||||
|
<Lock className="h-3 w-3" />
|
||||||
|
{t("configManaged", {
|
||||||
|
fallback: "Config Managed"
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
description={t("domainSettingDescription")}
|
description={t("domainSettingDescription")}
|
||||||
/>
|
/>
|
||||||
{env.flags.usePangolinDns && domain.failed ? (
|
{env.flags.usePangolinDns && domain.failed ? (
|
||||||
@@ -90,4 +107,4 @@ export default function DomainPageClient({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { formatAxiosError } from "@app/lib/api";
|
|||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import { Lock } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import CreateDomainForm from "@app/components/CreateDomainForm";
|
import CreateDomainForm from "@app/components/CreateDomainForm";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
@@ -72,7 +73,11 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: rawDomains, isRefetching, refetch } = useQuery({
|
const {
|
||||||
|
data: rawDomains,
|
||||||
|
isRefetching,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
...orgQueries.domains({ orgId }),
|
...orgQueries.domains({ orgId }),
|
||||||
initialData: domains as any,
|
initialData: domains as any,
|
||||||
refetchInterval: durationToMs(10, "seconds")
|
refetchInterval: durationToMs(10, "seconds")
|
||||||
@@ -80,12 +85,15 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
|
|
||||||
const tableData = useMemo(
|
const tableData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(rawDomains ?? []).map((d) => ({
|
(rawDomains ?? []).map(
|
||||||
...d,
|
(d) =>
|
||||||
baseDomain: toUnicode(d.baseDomain),
|
({
|
||||||
type: d.type ?? "",
|
...d,
|
||||||
errorMessage: d.errorMessage ?? null
|
baseDomain: toUnicode(d.baseDomain),
|
||||||
} as DomainRow)),
|
type: d.type ?? "",
|
||||||
|
errorMessage: d.errorMessage ?? null
|
||||||
|
}) as DomainRow
|
||||||
|
),
|
||||||
[rawDomains]
|
[rawDomains]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -198,12 +206,17 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Badge variant="red" className="cursor-help">
|
<Badge
|
||||||
|
variant="red"
|
||||||
|
className="cursor-help"
|
||||||
|
>
|
||||||
{t("failed", { fallback: "Failed" })}
|
{t("failed", { fallback: "Failed" })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<p className="break-words">{errorMessage}</p>
|
<p className="break-words">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -220,12 +233,17 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Badge variant="yellow" className="cursor-help">
|
<Badge
|
||||||
|
variant="yellow"
|
||||||
|
className="cursor-help"
|
||||||
|
>
|
||||||
{t("pending")}
|
{t("pending")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<p className="break-words">{errorMessage}</p>
|
<p className="break-words">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -253,6 +271,25 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const domain = row.original;
|
||||||
|
return (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{domain.baseDomain}
|
||||||
|
{domain.configManaged && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1 text-xs font-normal"
|
||||||
|
>
|
||||||
|
<Lock className="h-3 w-3" />
|
||||||
|
{t("configManaged", {
|
||||||
|
fallback: "Config Managed"
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...(env.env.flags.usePangolinDns ? [typeColumn] : []),
|
...(env.env.flags.usePangolinDns ? [typeColumn] : []),
|
||||||
@@ -283,16 +320,18 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
{t("viewSettings")}
|
{t("viewSettings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenuItem
|
{!domain.configManaged && (
|
||||||
onClick={() => {
|
<DropdownMenuItem
|
||||||
setSelectedDomain(domain);
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setSelectedDomain(domain);
|
||||||
}}
|
setIsDeleteModalOpen(true);
|
||||||
>
|
}}
|
||||||
<span className="text-red-500">
|
>
|
||||||
{t("delete")}
|
<span className="text-red-500">
|
||||||
</span>
|
{t("delete")}
|
||||||
</DropdownMenuItem>
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{domain.failed && (
|
{domain.failed && (
|
||||||
@@ -315,7 +354,9 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
href={`/${orgId}/settings/domains/${domain.domainId}`}
|
href={`/${orgId}/settings/domains/${domain.domainId}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outline"}>
|
<Button variant={"outline"}>
|
||||||
{t("edit")}
|
{domain.configManaged
|
||||||
|
? t("view", { fallback: "View" })
|
||||||
|
: t("edit")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
import { Textarea } from "@app/components/ui/textarea";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X, AlertCircle } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -56,6 +57,8 @@ export interface Destination {
|
|||||||
sendActionLogs: boolean;
|
sendActionLogs: boolean;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
sendRequestLogs: boolean;
|
sendRequestLogs: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
@@ -122,9 +125,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={h.value}
|
value={h.value}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateRow(i, "value", e.target.value)}
|
||||||
updateRow(i, "value", e.target.value)
|
|
||||||
}
|
|
||||||
placeholder={t("httpDestHeaderValuePlaceholder")}
|
placeholder={t("httpDestHeaderValuePlaceholder")}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
@@ -200,10 +201,7 @@ export function HttpDestinationCredenza({
|
|||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(raw);
|
const parsed = new URL(raw);
|
||||||
if (
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
parsed.protocol !== "http:" &&
|
|
||||||
parsed.protocol !== "https:"
|
|
||||||
) {
|
|
||||||
return t("httpDestUrlErrorHttpRequired");
|
return t("httpDestUrlErrorHttpRequired");
|
||||||
}
|
}
|
||||||
if (build === "saas" && parsed.protocol !== "https:") {
|
if (build === "saas" && parsed.protocol !== "https:") {
|
||||||
@@ -216,9 +214,7 @@ export function HttpDestinationCredenza({
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
cfg.name.trim() !== "" &&
|
cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null;
|
||||||
cfg.url.trim() !== "" &&
|
|
||||||
urlError === null;
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
@@ -253,10 +249,7 @@ export function HttpDestinationCredenza({
|
|||||||
title: editing
|
title: editing
|
||||||
? t("httpDestUpdateFailed")
|
? t("httpDestUpdateFailed")
|
||||||
: t("httpDestCreateFailed"),
|
: t("httpDestCreateFailed"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||||
e,
|
|
||||||
t("streamingUnexpectedError")
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
@@ -280,6 +273,14 @@ export function HttpDestinationCredenza({
|
|||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
|
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
{editing?.lastError && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="break-words">
|
||||||
|
{editing.lastError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<HorizontalTabs
|
<HorizontalTabs
|
||||||
clientSide
|
clientSide
|
||||||
items={[
|
items={[
|
||||||
@@ -357,7 +358,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestAuthNoneTitle")}
|
{t("httpDestAuthNoneTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthNoneDescription")}
|
{t(
|
||||||
|
"httpDestAuthNoneDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,15 +378,21 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-bearer"
|
htmlFor="auth-bearer"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthBearerTitle")}
|
{t(
|
||||||
|
"httpDestAuthBearerTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthBearerDescription")}
|
{t(
|
||||||
|
"httpDestAuthBearerDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "bearer" && (
|
{cfg.authType === "bearer" && (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthBearerPlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthBearerPlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.bearerToken ?? ""
|
cfg.bearerToken ?? ""
|
||||||
}
|
}
|
||||||
@@ -411,15 +420,21 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-basic"
|
htmlFor="auth-basic"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthBasicTitle")}
|
{t(
|
||||||
|
"httpDestAuthBasicTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthBasicDescription")}
|
{t(
|
||||||
|
"httpDestAuthBasicDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "basic" && (
|
{cfg.authType === "basic" && (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthBasicPlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthBasicPlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.basicCredentials ??
|
cfg.basicCredentials ??
|
||||||
""
|
""
|
||||||
@@ -448,16 +463,22 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-custom"
|
htmlFor="auth-custom"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthCustomTitle")}
|
{t(
|
||||||
|
"httpDestAuthCustomTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthCustomDescription")}
|
{t(
|
||||||
|
"httpDestAuthCustomDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "custom" && (
|
{cfg.authType === "custom" && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthCustomHeaderNamePlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.customHeaderName ??
|
cfg.customHeaderName ??
|
||||||
""
|
""
|
||||||
@@ -472,7 +493,9 @@ export function HttpDestinationCredenza({
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthCustomHeaderValuePlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.customHeaderValue ??
|
cfg.customHeaderValue ??
|
||||||
""
|
""
|
||||||
@@ -593,10 +616,14 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="fmt-json-array"
|
htmlFor="fmt-json-array"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestFormatJsonArrayTitle")}
|
{t(
|
||||||
|
"httpDestFormatJsonArrayTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatJsonArrayDescription")}
|
{t(
|
||||||
|
"httpDestFormatJsonArrayDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -616,7 +643,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestFormatNdjsonTitle")}
|
{t("httpDestFormatNdjsonTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatNdjsonDescription")}
|
{t(
|
||||||
|
"httpDestFormatNdjsonDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -636,7 +665,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestFormatSingleTitle")}
|
{t("httpDestFormatSingleTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatSingleDescription")}
|
{t(
|
||||||
|
"httpDestFormatSingleDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -717,7 +748,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestConnectionLogsTitle")}
|
{t("httpDestConnectionLogsTitle")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestConnectionLogsDescription")}
|
{t(
|
||||||
|
"httpDestConnectionLogsDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -739,7 +772,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestRequestLogsTitle")}
|
{t("httpDestRequestLogsTitle")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestRequestLogsDescription")}
|
{t(
|
||||||
|
"httpDestRequestLogsDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -764,10 +799,12 @@ export function HttpDestinationCredenza({
|
|||||||
loading={saving}
|
loading={saving}
|
||||||
disabled={!isValid || saving}
|
disabled={!isValid || saving}
|
||||||
>
|
>
|
||||||
{editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
|
{editing
|
||||||
|
? t("httpDestSaveChanges")
|
||||||
|
: t("httpDestCreateDestination")}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ChevronsUpDown, ExternalLink } from "lucide-react";
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
ExternalLink
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -50,11 +55,13 @@ import {
|
|||||||
formatMultiSitesSelectorLabel
|
formatMultiSitesSelectorLabel
|
||||||
} from "./multi-site-selector";
|
} from "./multi-site-selector";
|
||||||
import type { Selectedsite } from "./site-selector";
|
import type { Selectedsite } from "./site-selector";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
|
||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import CertificateStatus from "@app/components/CertificateStatus";
|
import CertificateStatus from "@app/components/CertificateStatus";
|
||||||
|
import { UsersSelector } from "./users-selector";
|
||||||
|
import { RolesSelector } from "./roles-selector";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// --- Helpers (shared) ---
|
// --- Helpers (shared) ---
|
||||||
@@ -833,12 +840,16 @@ export function InternalResourceForm({
|
|||||||
modeCidrKey
|
modeCidrKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
...(!disableEnterpriseFeatures
|
||||||
value: "http",
|
? [
|
||||||
label: t(
|
{
|
||||||
modeHttpKey
|
value: "http" as const,
|
||||||
)
|
label: t(
|
||||||
}
|
modeHttpKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
@@ -1484,40 +1495,22 @@ export function InternalResourceForm({
|
|||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("roles")}</FormLabel>
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<RolesSelector
|
||||||
{...field}
|
selectedRoles={
|
||||||
activeTagIndex={
|
field.value ?? []
|
||||||
activeRolesTagIndex
|
|
||||||
}
|
}
|
||||||
setActiveTagIndex={
|
orgId={orgId}
|
||||||
setActiveRolesTagIndex
|
onSelectRoles={(
|
||||||
}
|
newUsers
|
||||||
placeholder={t(
|
) => {
|
||||||
"accessRoleSelect2"
|
|
||||||
)}
|
|
||||||
size="sm"
|
|
||||||
tags={
|
|
||||||
form.getValues()
|
|
||||||
.roles ?? []
|
|
||||||
}
|
|
||||||
setTags={(newRoles) =>
|
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"roles",
|
"roles",
|
||||||
newRoles as [
|
newUsers as [
|
||||||
Tag,
|
Tag,
|
||||||
...Tag[]
|
...Tag[]
|
||||||
]
|
]
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
enableAutocomplete
|
|
||||||
autocompleteOptions={
|
|
||||||
allRoles
|
|
||||||
}
|
|
||||||
allowDuplicates={false}
|
|
||||||
restrictTagsToAutocompleteOptions={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
sortTags={true}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1530,43 +1523,21 @@ export function InternalResourceForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("users")}</FormLabel>
|
<FormLabel>{t("users")}</FormLabel>
|
||||||
<FormControl>
|
<UsersSelector
|
||||||
<TagInput
|
selectedUsers={
|
||||||
{...field}
|
field.value ?? []
|
||||||
activeTagIndex={
|
}
|
||||||
activeUsersTagIndex
|
orgId={orgId}
|
||||||
}
|
onSelectUsers={(newUsers) => {
|
||||||
setActiveTagIndex={
|
form.setValue(
|
||||||
setActiveUsersTagIndex
|
"users",
|
||||||
}
|
newUsers as [
|
||||||
placeholder={t(
|
Tag,
|
||||||
"accessUserSelect"
|
...Tag[]
|
||||||
)}
|
]
|
||||||
tags={
|
);
|
||||||
form.getValues()
|
}}
|
||||||
.users ?? []
|
/>
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
setTags={(newUsers) =>
|
|
||||||
form.setValue(
|
|
||||||
"users",
|
|
||||||
newUsers as [
|
|
||||||
Tag,
|
|
||||||
...Tag[]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
enableAutocomplete={true}
|
|
||||||
autocompleteOptions={
|
|
||||||
allUsers
|
|
||||||
}
|
|
||||||
allowDuplicates={false}
|
|
||||||
restrictTagsToAutocompleteOptions={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
sortTags={true}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -1580,73 +1551,20 @@ export function InternalResourceForm({
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("machineClients")}
|
{t("machineClients")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover>
|
<MachinesSelector
|
||||||
<PopoverTrigger asChild>
|
selectedMachines={
|
||||||
<FormControl>
|
field.value ?? []
|
||||||
<Button
|
}
|
||||||
variant="outline"
|
orgId={orgId}
|
||||||
role="combobox"
|
onSelectMachines={(
|
||||||
className={cn(
|
machines
|
||||||
"justify-between w-full",
|
) => {
|
||||||
"text-muted-foreground pl-1.5"
|
form.setValue(
|
||||||
)}
|
"clients",
|
||||||
>
|
machines
|
||||||
<span
|
);
|
||||||
className={cn(
|
}}
|
||||||
"inline-flex items-center gap-1",
|
/>
|
||||||
"overflow-x-auto"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
field.value ??
|
|
||||||
[]
|
|
||||||
).map(
|
|
||||||
(
|
|
||||||
client
|
|
||||||
) => (
|
|
||||||
<span
|
|
||||||
key={
|
|
||||||
client.clientId
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"bg-muted-foreground/20 font-normal text-foreground rounded-sm",
|
|
||||||
"py-1 px-1.5 text-xs"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
client.name
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<span className="pl-1 font-normal">
|
|
||||||
{t(
|
|
||||||
"accessClientSelect"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<MachinesSelector
|
|
||||||
selectedMachines={
|
|
||||||
field.value ??
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
orgId={orgId}
|
|
||||||
onSelectMachines={(
|
|
||||||
machines
|
|
||||||
) => {
|
|
||||||
form.setValue(
|
|
||||||
"clients",
|
|
||||||
machines
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function InviteStatusCard({
|
|||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else if (!user && type === "not_logged_in") {
|
} else if (!user && type === "not_logged_in") {
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,7 +113,7 @@ export default function InviteStatusCard({
|
|||||||
async function goToLogin() {
|
async function goToLogin() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,9 +129,7 @@ export function LayoutSidebar({
|
|||||||
user.serverAdmin || Boolean(currentOrg?.isOwner || currentOrg?.isAdmin);
|
user.serverAdmin || Boolean(currentOrg?.isOwner || currentOrg?.isAdmin);
|
||||||
|
|
||||||
const showTrial =
|
const showTrial =
|
||||||
build === "saas" &&
|
build === "saas" && Boolean(orgId) && subscriptionContext?.isTrial;
|
||||||
Boolean(orgId) &&
|
|
||||||
subscriptionContext?.isTrial;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -240,11 +238,16 @@ export function LayoutSidebar({
|
|||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
) : <div className="mt-0.2"></div>}
|
) : (
|
||||||
|
<div className="mt-0.2"></div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showTrial && (
|
{showTrial && (
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ShowTrialCard isCollapsed={isSidebarCollapsed} />
|
<ShowTrialCard
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
isOwner={Boolean(currentOrg?.isOwner)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
|
|
||||||
// Update Resource type to include site information
|
// Update Resource type to include site information
|
||||||
type Resource = {
|
type Resource = {
|
||||||
@@ -64,6 +65,8 @@ type SiteResource = {
|
|||||||
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;
|
||||||
@@ -123,6 +126,7 @@ const ResourceFavicon = ({
|
|||||||
|
|
||||||
// Resource Info component
|
// Resource Info component
|
||||||
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
|
const t = useTranslations();
|
||||||
const hasAuthMethods =
|
const hasAuthMethods =
|
||||||
resource.sso ||
|
resource.sso ||
|
||||||
resource.password ||
|
resource.password ||
|
||||||
@@ -141,7 +145,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
{/* Site Information */}
|
{/* Site Information */}
|
||||||
{resource.siteName && (
|
{resource.siteName && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium mb-1.5">Site</div>
|
<div className="text-xs font-medium mb-1.5">
|
||||||
|
{t("site")}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
||||||
<span className="text-sm">{resource.siteName}</span>
|
<span className="text-sm">{resource.siteName}</span>
|
||||||
@@ -157,7 +163,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">
|
||||||
Authentication Methods
|
{t("memberPortalAuthMethods")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{resource.sso && (
|
{resource.sso && (
|
||||||
@@ -166,7 +172,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Single Sign-On (SSO)
|
{t("memberPortalSso")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -176,7 +182,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Password Protected
|
{t("memberPortalPasswordProtected")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -185,7 +191,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
||||||
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">PIN Code</span>
|
<span className="text-sm">
|
||||||
|
{t("memberPortalPinCode")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.whitelist && (
|
{resource.whitelist && (
|
||||||
@@ -193,7 +201,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
||||||
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">Email Whitelist</span>
|
<span className="text-sm">
|
||||||
|
{t("memberPortalEmailWhitelist")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +218,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||||
<span className="text-sm text-destructive">
|
<span className="text-sm text-destructive">
|
||||||
Resource Disabled
|
{t("memberPortalResourceDisabled")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,6 +243,7 @@ const PaginationControls = ({
|
|||||||
totalItems: number;
|
totalItems: number;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations();
|
||||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||||
|
|
||||||
@@ -241,7 +252,11 @@ const PaginationControls = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Showing {startItem}-{endItem} of {totalItems} resources
|
{t("memberPortalShowingResources", {
|
||||||
|
start: startItem,
|
||||||
|
end: endItem,
|
||||||
|
total: totalItems
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -253,7 +268,7 @@ const PaginationControls = ({
|
|||||||
className="gap-1"
|
className="gap-1"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
Previous
|
{t("memberPortalPrevious")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -309,7 +324,7 @@ const PaginationControls = ({
|
|||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
>
|
>
|
||||||
Next
|
{t("memberPortalNext")}
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,13 +404,11 @@ export default function MemberResourcesPortal({
|
|||||||
response.data.data.siteResources || []
|
response.data.data.siteResources || []
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setError("Failed to load resources");
|
setError(t("memberPortalFailedToLoad"));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching user resources:", err);
|
console.error("Error fetching user resources:", err);
|
||||||
setError(
|
setError(t("memberPortalFailedToLoadDescription"));
|
||||||
"Failed to load resources. Please check your connection and try again."
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@@ -526,8 +539,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Resources"
|
title={t("memberPortalTitle")}
|
||||||
description="Resources you have access to in this organization"
|
description={t("memberPortalDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search and Sort Controls - Skeleton */}
|
{/* Search and Sort Controls - Skeleton */}
|
||||||
@@ -554,8 +567,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Resources"
|
title={t("memberPortalTitle")}
|
||||||
description="Resources you have access to in this organization"
|
description={t("memberPortalDescription")}
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
@@ -563,7 +576,7 @@ export default function MemberResourcesPortal({
|
|||||||
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||||
Unable to Load Resources
|
{t("memberPortalUnableToLoad")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||||
{error}
|
{error}
|
||||||
@@ -574,7 +587,7 @@ export default function MemberResourcesPortal({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
Try Again
|
{t("memberPortalTryAgain")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -585,8 +598,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Resources"
|
title={t("memberPortalTitle")}
|
||||||
description="Resources you have access to in this organization"
|
description={t("memberPortalDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search and Sort Controls with Refresh */}
|
{/* Search and Sort Controls with Refresh */}
|
||||||
@@ -595,7 +608,7 @@ export default function MemberResourcesPortal({
|
|||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative w-full sm:w-80">
|
<div className="relative w-full sm:w-80">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search resources..."
|
placeholder={t("resourcesSearch")}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-8 bg-card"
|
className="w-full pl-8 bg-card"
|
||||||
@@ -607,26 +620,28 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="w-full sm:w-36">
|
<div className="w-full sm:w-36">
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectTrigger className="bg-card">
|
<SelectTrigger className="bg-card">
|
||||||
<SelectValue placeholder="Sort by..." />
|
<SelectValue
|
||||||
|
placeholder={t("memberPortalSortBy")}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="name-asc">
|
<SelectItem value="name-asc">
|
||||||
Name A-Z
|
{t("memberPortalSortNameAsc")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="name-desc">
|
<SelectItem value="name-desc">
|
||||||
Name Z-A
|
{t("memberPortalSortNameDesc")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="domain-asc">
|
<SelectItem value="domain-asc">
|
||||||
Domain A-Z
|
{t("memberPortalSortDomainAsc")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="domain-desc">
|
<SelectItem value="domain-desc">
|
||||||
Domain Z-A
|
{t("memberPortalSortDomainDesc")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="status-enabled">
|
<SelectItem value="status-enabled">
|
||||||
Enabled First
|
{t("memberPortalSortEnabledFirst")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="status-disabled">
|
<SelectItem value="status-disabled">
|
||||||
Disabled First
|
{t("memberPortalSortDisabledFirst")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -644,7 +659,7 @@ export default function MemberResourcesPortal({
|
|||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
Refresh
|
{t("memberPortalRefresh")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -663,13 +678,15 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? "No Resources Found"
|
? t("memberPortalNoResourcesFound")
|
||||||
: "No Resources Available"}
|
: t("memberPortalNoResourcesAvailable")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.`
|
? t("memberPortalNoResourcesMatchSearch", {
|
||||||
: "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}
|
query: searchQuery
|
||||||
|
})
|
||||||
|
: t("memberPortalNoResourcesAccess")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
{searchQuery ? (
|
{searchQuery ? (
|
||||||
@@ -678,7 +695,7 @@ export default function MemberResourcesPortal({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
Clear Search
|
{t("memberPortalClearSearch")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -690,7 +707,7 @@ export default function MemberResourcesPortal({
|
|||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
Refresh Resources
|
{t("memberPortalRefreshResources")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -704,11 +721,12 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
<Globe className="h-5 w-5" />
|
<Globe className="h-5 w-5" />
|
||||||
Public Resources
|
{t("memberPortalPublicResources")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Web applications and services accessible via
|
{t(
|
||||||
browser
|
"memberPortalPublicResourcesDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||||
@@ -768,9 +786,12 @@ export default function MemberResourcesPortal({
|
|||||||
resource.domain
|
resource.domain
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied to clipboard",
|
title: t(
|
||||||
description:
|
"memberPortalCopiedToClipboard"
|
||||||
"Resource URL has been copied to your clipboard.",
|
),
|
||||||
|
description: t(
|
||||||
|
"memberPortalCopiedUrlDescription"
|
||||||
|
),
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -791,7 +812,7 @@ export default function MemberResourcesPortal({
|
|||||||
disabled={!resource.enabled}
|
disabled={!resource.enabled}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||||
Open Resource
|
{t("memberPortalOpenResource")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -806,11 +827,12 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
<Combine className="h-5 w-5" />
|
<Combine className="h-5 w-5" />
|
||||||
Private Resources
|
{t("memberPortalPrivateResources")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Internal network resources accessible via
|
{t(
|
||||||
client
|
"memberPortalPrivateResourcesDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||||
@@ -843,11 +865,16 @@ export default function MemberResourcesPortal({
|
|||||||
<InfoPopup>
|
<InfoPopup>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">
|
||||||
Resource Details
|
{t(
|
||||||
|
"memberPortalResourceDetails"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Mode:
|
{t(
|
||||||
|
"memberPortalMode"
|
||||||
|
)}
|
||||||
|
:
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground capitalize">
|
<span className="ml-2 text-muted-foreground capitalize">
|
||||||
{
|
{
|
||||||
@@ -858,7 +885,10 @@ export default function MemberResourcesPortal({
|
|||||||
{siteResource.protocol && (
|
{siteResource.protocol && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Protocol:
|
{t(
|
||||||
|
"protocol"
|
||||||
|
)}
|
||||||
|
:
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground uppercase">
|
<span className="ml-2 text-muted-foreground uppercase">
|
||||||
{
|
{
|
||||||
@@ -869,7 +899,10 @@ export default function MemberResourcesPortal({
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Destination:
|
{t(
|
||||||
|
"memberPortalDestination"
|
||||||
|
)}
|
||||||
|
:
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 text-muted-foreground">
|
||||||
{
|
{
|
||||||
@@ -880,7 +913,10 @@ export default function MemberResourcesPortal({
|
|||||||
{siteResource.alias && (
|
{siteResource.alias && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Alias:
|
{t(
|
||||||
|
"memberPortalAlias"
|
||||||
|
)}
|
||||||
|
:
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 text-muted-foreground">
|
||||||
{
|
{
|
||||||
@@ -891,14 +927,21 @@ export default function MemberResourcesPortal({
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Status:
|
{t(
|
||||||
|
"status"
|
||||||
|
)}
|
||||||
|
:
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
|
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
|
||||||
>
|
>
|
||||||
{siteResource.enabled
|
{siteResource.enabled
|
||||||
? "Enabled"
|
? t(
|
||||||
: "Disabled"}
|
"enabled"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"disabled"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -907,7 +950,14 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
{siteResource.alias ? (
|
{siteResource.mode === "http" &&
|
||||||
|
siteResource.fullDomain ? (
|
||||||
|
/* HTTP mode - show as clickable link */
|
||||||
|
<CopyToClipboard
|
||||||
|
text={`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`}
|
||||||
|
isLink={true}
|
||||||
|
/>
|
||||||
|
) : siteResource.alias ? (
|
||||||
<>
|
<>
|
||||||
{/* Alias as primary */}
|
{/* Alias as primary */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
@@ -925,9 +975,13 @@ export default function MemberResourcesPortal({
|
|||||||
siteResource.alias!
|
siteResource.alias!
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied to clipboard",
|
title: t(
|
||||||
|
"memberPortalCopiedToClipboard"
|
||||||
|
),
|
||||||
description:
|
description:
|
||||||
"Resource alias has been copied to your clipboard.",
|
t(
|
||||||
|
"memberPortalCopiedAliasDescription"
|
||||||
|
),
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -959,9 +1013,13 @@ export default function MemberResourcesPortal({
|
|||||||
siteResource.destination
|
siteResource.destination
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied to clipboard",
|
title: t(
|
||||||
|
"memberPortalCopiedToClipboard"
|
||||||
|
),
|
||||||
description:
|
description:
|
||||||
"Resource destination has been copied to your clipboard.",
|
t(
|
||||||
|
"memberPortalCopiedDestinationDescription"
|
||||||
|
),
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -973,10 +1031,34 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 pt-0 mt-auto">
|
<div className="p-6 pt-0 mt-auto space-y-2">
|
||||||
|
{siteResource.mode === "http" &&
|
||||||
|
siteResource.fullDomain ? (
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`,
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full h-9"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={
|
||||||
|
!siteResource.enabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||||
|
{t(
|
||||||
|
"memberPortalOpenResource"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
|
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
|
||||||
<Combine className="h-3.5 w-3.5 mr-2" />
|
<Combine className="h-3.5 w-3.5 mr-2" />
|
||||||
Requires Client Connection
|
{t(
|
||||||
|
"memberPortalRequiresClientConnection"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Link from "next/link";
|
|||||||
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||||
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 { build } from "@server/build";
|
||||||
|
|
||||||
type OrgLoginPageProps = {
|
type OrgLoginPageProps = {
|
||||||
loginPage: LoadLoginPageResponse | undefined;
|
loginPage: LoadLoginPageResponse | undefined;
|
||||||
@@ -52,19 +53,21 @@ export default async function OrgLoginPage({
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-center mb-2">
|
{build !== "enterprise" || !env.branding.hidePoweredBy ? (
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="text-center mb-2">
|
||||||
{t("poweredBy")}{" "}
|
<span className="text-sm text-muted-foreground">
|
||||||
<Link
|
{t("poweredBy")}{" "}
|
||||||
href="https://pangolin.net/"
|
<Link
|
||||||
target="_blank"
|
href="https://pangolin.net/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="underline"
|
||||||
{env.branding.appName || "Pangolin"}
|
>
|
||||||
</Link>
|
{env.branding.appName || "Pangolin"}
|
||||||
</span>
|
</Link>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
{branding?.logoUrl && (
|
{branding?.logoUrl && (
|
||||||
|
|||||||
@@ -8,51 +8,42 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
|
||||||
|
|
||||||
export type RoleTag = {
|
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||||
id: string;
|
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OrgRolesTagFieldProps<TFieldValues extends FieldValues> = {
|
type OrgRolesTagFieldProps<TFieldValues extends FieldValues> = {
|
||||||
form: Pick<UseFormReturn<TFieldValues>, "control" | "getValues" | "setValue">;
|
form: Pick<
|
||||||
|
UseFormReturn<TFieldValues>,
|
||||||
|
"control" | "getValues" | "setValue"
|
||||||
|
>;
|
||||||
|
orgId: string;
|
||||||
/** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */
|
/** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */
|
||||||
name?: Path<TFieldValues>;
|
name?: Path<TFieldValues>;
|
||||||
label: string;
|
label?: string;
|
||||||
placeholder: string;
|
|
||||||
allRoleOptions: Tag[];
|
|
||||||
supportsMultipleRolesPerUser: boolean;
|
supportsMultipleRolesPerUser: boolean;
|
||||||
showMultiRolePaywallMessage: boolean;
|
showMultiRolePaywallMessage: boolean;
|
||||||
paywallMessage: string;
|
paywallMessage: string;
|
||||||
loading?: boolean;
|
disabled?: boolean;
|
||||||
activeTagIndex: number | null;
|
|
||||||
setActiveTagIndex: Dispatch<SetStateAction<number | null>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
||||||
form,
|
form,
|
||||||
name = "roles" as Path<TFieldValues>,
|
name = "roles" as Path<TFieldValues>,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
orgId,
|
||||||
allRoleOptions,
|
|
||||||
supportsMultipleRolesPerUser,
|
supportsMultipleRolesPerUser,
|
||||||
showMultiRolePaywallMessage,
|
showMultiRolePaywallMessage,
|
||||||
paywallMessage,
|
paywallMessage,
|
||||||
loading = false,
|
disabled
|
||||||
activeTagIndex,
|
|
||||||
setActiveTagIndex
|
|
||||||
}: OrgRolesTagFieldProps<TFieldValues>) {
|
}: OrgRolesTagFieldProps<TFieldValues>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
function setRoleTags(updater: Tag[] | ((prev: Tag[]) => Tag[])) {
|
function setRoleTags(nextValue: SelectedRole[]) {
|
||||||
const prev = form.getValues(name) as Tag[];
|
const prev = form.getValues(name) as SelectedRole[];
|
||||||
const nextValue =
|
|
||||||
typeof updater === "function" ? updater(prev) : updater;
|
|
||||||
const next = supportsMultipleRolesPerUser
|
const next = supportsMultipleRolesPerUser
|
||||||
? nextValue
|
? nextValue
|
||||||
: nextValue.length > 1
|
: nextValue.length > 1
|
||||||
@@ -88,22 +79,13 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{label}</FormLabel>
|
<FormLabel>{label ?? t("roles")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<RolesSelector
|
||||||
{...field}
|
orgId={orgId}
|
||||||
activeTagIndex={activeTagIndex}
|
selectedRoles={field.value ?? []}
|
||||||
setActiveTagIndex={setActiveTagIndex}
|
onSelectRoles={setRoleTags}
|
||||||
placeholder={placeholder}
|
disabled={disabled}
|
||||||
size="sm"
|
|
||||||
tags={field.value}
|
|
||||||
setTags={setRoleTags}
|
|
||||||
enableAutocomplete={true}
|
|
||||||
autocompleteOptions={allRoleOptions}
|
|
||||||
allowDuplicates={false}
|
|
||||||
restrictTagsToAutocompleteOptions={true}
|
|
||||||
sortTags={true}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{showMultiRolePaywallMessage && (
|
{showMultiRolePaywallMessage && (
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export type ResourceRow = {
|
|||||||
targets?: TargetHealth[];
|
targets?: TargetHealth[];
|
||||||
health?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
health?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
||||||
sites: ResourceSiteRow[];
|
sites: ResourceSiteRow[];
|
||||||
|
wildcard?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function StatusIcon({
|
function StatusIcon({
|
||||||
@@ -570,10 +571,14 @@ export default function ProxyResourcesTable({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="">
|
<div className="">
|
||||||
<CopyToClipboard
|
{!resourceRow.wildcard ? (
|
||||||
text={resourceRow.domain}
|
<CopyToClipboard
|
||||||
isLink={true}
|
text={resourceRow.domain}
|
||||||
/>
|
isLink={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{resourceRow.domain}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -375,7 +375,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
{!accessDenied ? (
|
{!accessDenied ? (
|
||||||
<div>
|
<div>
|
||||||
{isUnlocked() && build === "enterprise" ? (
|
{isUnlocked() && build === "enterprise" ? (
|
||||||
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
!env.branding.resourceAuthPage?.hidePoweredBy &&
|
||||||
|
!env.branding.hidePoweredBy && (
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{t("poweredBy")}{" "}
|
{t("poweredBy")}{" "}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { RolesSelector } from "./roles-selector";
|
||||||
|
|
||||||
export type RoleMappingRoleOption = {
|
export type RoleMappingRoleOption = {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
@@ -38,6 +39,8 @@ export type RoleMappingConfigFieldsProps = {
|
|||||||
fieldIdPrefix?: string;
|
fieldIdPrefix?: string;
|
||||||
/** When true, show extra hint for global default policies (no org role list). */
|
/** When true, show extra hint for global default policies (no org role list). */
|
||||||
showFreeformRoleNamesHint?: boolean;
|
showFreeformRoleNamesHint?: boolean;
|
||||||
|
/** Org ID to use for role lookup. Falls back to URL params when not provided. */
|
||||||
|
orgId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RoleMappingConfigFields({
|
export default function RoleMappingConfigFields({
|
||||||
@@ -53,14 +56,12 @@ export default function RoleMappingConfigFields({
|
|||||||
rawExpression,
|
rawExpression,
|
||||||
onRawExpressionChange,
|
onRawExpressionChange,
|
||||||
fieldIdPrefix = "role-mapping",
|
fieldIdPrefix = "role-mapping",
|
||||||
showFreeformRoleNamesHint = false
|
showFreeformRoleNamesHint = false,
|
||||||
|
orgId
|
||||||
}: RoleMappingConfigFieldsProps) {
|
}: RoleMappingConfigFieldsProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
||||||
const showSingleRoleDisclaimer =
|
const showSingleRoleDisclaimer =
|
||||||
@@ -94,6 +95,10 @@ export default function RoleMappingConfigFields({
|
|||||||
}
|
}
|
||||||
}, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]);
|
}, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]);
|
||||||
|
|
||||||
|
const [fixedRolesActiveTagIndex, setFixedRolesActiveTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
||||||
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
||||||
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
||||||
@@ -160,58 +165,94 @@ export default function RoleMappingConfigFields({
|
|||||||
|
|
||||||
{roleMappingMode === "fixedRoles" && (
|
{roleMappingMode === "fixedRoles" && (
|
||||||
<div className="space-y-2 min-w-0 max-w-full">
|
<div className="space-y-2 min-w-0 max-w-full">
|
||||||
<TagInput
|
{restrictToOrgRoles ? (
|
||||||
tags={fixedRoleNames.map((name) => ({
|
<RolesSelector
|
||||||
id: name,
|
selectedRoles={fixedRoleNames.map((name) => ({
|
||||||
text: name
|
|
||||||
}))}
|
|
||||||
setTags={(nextTags) => {
|
|
||||||
const prevTags = fixedRoleNames.map((name) => ({
|
|
||||||
id: name,
|
id: name,
|
||||||
text: name
|
text: name
|
||||||
}));
|
}))}
|
||||||
const next =
|
mapRolesByName
|
||||||
typeof nextTags === "function"
|
orgId={orgId as string}
|
||||||
? nextTags(prevTags)
|
onSelectRoles={(nextTags) => {
|
||||||
: nextTags;
|
let names = [
|
||||||
|
...new Set(nextTags.map((tag) => tag.text))
|
||||||
|
];
|
||||||
|
|
||||||
let names = [
|
if (!supportsMultipleRolesPerUser) {
|
||||||
...new Set(next.map((tag) => tag.text))
|
if (
|
||||||
];
|
names.length === 0 &&
|
||||||
|
fixedRoleNames.length > 0
|
||||||
if (!supportsMultipleRolesPerUser) {
|
) {
|
||||||
if (
|
onFixedRoleNamesChange([
|
||||||
names.length === 0 &&
|
fixedRoleNames[
|
||||||
fixedRoleNames.length > 0
|
fixedRoleNames.length - 1
|
||||||
) {
|
]!
|
||||||
onFixedRoleNamesChange([
|
]);
|
||||||
fixedRoleNames[
|
return;
|
||||||
fixedRoleNames.length - 1
|
}
|
||||||
]!
|
if (names.length > 1) {
|
||||||
]);
|
names = [names[names.length - 1]!];
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
if (names.length > 1) {
|
|
||||||
names = [names[names.length - 1]!];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFixedRoleNamesChange(names);
|
onFixedRoleNamesChange(names);
|
||||||
}}
|
}}
|
||||||
activeTagIndex={activeFixedRoleTagIndex}
|
/>
|
||||||
setActiveTagIndex={setActiveFixedRoleTagIndex}
|
) : (
|
||||||
placeholder={
|
<TagInput
|
||||||
restrictToOrgRoles
|
tags={fixedRoleNames.map((name) => ({
|
||||||
? t("roleMappingFixedRolesPlaceholderSelect")
|
id: name,
|
||||||
: t("roleMappingFixedRolesPlaceholderFreeform")
|
text: name
|
||||||
}
|
}))}
|
||||||
enableAutocomplete={restrictToOrgRoles}
|
setTags={(nextTags) => {
|
||||||
autocompleteOptions={roleOptions}
|
const prev = fixedRoleNames.map((name) => ({
|
||||||
restrictTagsToAutocompleteOptions={restrictToOrgRoles}
|
id: name,
|
||||||
allowDuplicates={false}
|
text: name
|
||||||
sortTags={true}
|
}));
|
||||||
size="sm"
|
const next =
|
||||||
/>
|
typeof nextTags === "function"
|
||||||
|
? nextTags(prev)
|
||||||
|
: nextTags;
|
||||||
|
|
||||||
|
let names = [
|
||||||
|
...new Set(next.map((tag) => tag.text))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!supportsMultipleRolesPerUser) {
|
||||||
|
if (
|
||||||
|
names.length === 0 &&
|
||||||
|
fixedRoleNames.length > 0
|
||||||
|
) {
|
||||||
|
onFixedRoleNamesChange([
|
||||||
|
fixedRoleNames[
|
||||||
|
fixedRoleNames.length - 1
|
||||||
|
]!
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
|
names = [names[names.length - 1]!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFixedRoleNamesChange(names);
|
||||||
|
}}
|
||||||
|
activeTagIndex={fixedRolesActiveTagIndex}
|
||||||
|
setActiveTagIndex={setFixedRolesActiveTagIndex}
|
||||||
|
placeholder={t(
|
||||||
|
"roleMappingAssignRolesPlaceholderFreeform"
|
||||||
|
)}
|
||||||
|
enableAutocomplete={false}
|
||||||
|
autocompleteOptions={roleOptions}
|
||||||
|
restrictTagsToAutocompleteOptions={false}
|
||||||
|
allowDuplicates={false}
|
||||||
|
sortTags={true}
|
||||||
|
size="sm"
|
||||||
|
styleClasses={{
|
||||||
|
inlineTagsContainer: "min-w-0 max-w-full"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{showFreeformRoleNamesHint
|
{showFreeformRoleNamesHint
|
||||||
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
||||||
@@ -261,6 +302,7 @@ export default function RoleMappingConfigFields({
|
|||||||
showFreeformRoleNamesHint={
|
showFreeformRoleNamesHint={
|
||||||
showFreeformRoleNamesHint
|
showFreeformRoleNamesHint
|
||||||
}
|
}
|
||||||
|
orgId={orgId}
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -337,7 +379,8 @@ function BuilderRuleRow({
|
|||||||
supportsMultipleRolesPerUser,
|
supportsMultipleRolesPerUser,
|
||||||
showRemoveButton,
|
showRemoveButton,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove
|
onRemove,
|
||||||
|
orgId
|
||||||
}: {
|
}: {
|
||||||
rule: MappingBuilderRule;
|
rule: MappingBuilderRule;
|
||||||
roleOptions: Tag[];
|
roleOptions: Tag[];
|
||||||
@@ -349,6 +392,7 @@ function BuilderRuleRow({
|
|||||||
showRemoveButton: boolean;
|
showRemoveButton: boolean;
|
||||||
onChange: (rule: MappingBuilderRule) => void;
|
onChange: (rule: MappingBuilderRule) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
|
orgId?: string;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||||
@@ -378,67 +422,109 @@ function BuilderRuleRow({
|
|||||||
{t("roleMappingAssignRoles")}
|
{t("roleMappingAssignRoles")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="min-w-0 max-w-full">
|
<div className="min-w-0 max-w-full">
|
||||||
<TagInput
|
{restrictToOrgRoles ? (
|
||||||
tags={rule.roleNames.map((name) => ({
|
<RolesSelector
|
||||||
id: name,
|
selectedRoles={rule.roleNames.map((name) => ({
|
||||||
text: name
|
|
||||||
}))}
|
|
||||||
setTags={(nextTags) => {
|
|
||||||
const prevRoleTags = rule.roleNames.map((name) => ({
|
|
||||||
id: name,
|
id: name,
|
||||||
text: name
|
text: name
|
||||||
}));
|
}))}
|
||||||
const next =
|
buttonText={t("roleMappingAssignRoles")}
|
||||||
typeof nextTags === "function"
|
mapRolesByName
|
||||||
? nextTags(prevRoleTags)
|
orgId={orgId as string}
|
||||||
: nextTags;
|
onSelectRoles={(nextTags) => {
|
||||||
|
let names = [
|
||||||
|
...new Set(nextTags.map((tag) => tag.text))
|
||||||
|
];
|
||||||
|
|
||||||
let names = [
|
if (!supportsMultipleRolesPerUser) {
|
||||||
...new Set(next.map((tag) => tag.text))
|
if (
|
||||||
];
|
names.length === 0 &&
|
||||||
|
rule.roleNames.length > 0
|
||||||
if (!supportsMultipleRolesPerUser) {
|
) {
|
||||||
if (
|
onChange({
|
||||||
names.length === 0 &&
|
...rule,
|
||||||
rule.roleNames.length > 0
|
roleNames: [
|
||||||
) {
|
rule.roleNames[
|
||||||
onChange({
|
rule.roleNames.length - 1
|
||||||
...rule,
|
]!
|
||||||
roleNames: [
|
]
|
||||||
rule.roleNames[
|
});
|
||||||
rule.roleNames.length - 1
|
return;
|
||||||
]!
|
}
|
||||||
]
|
if (names.length > 1) {
|
||||||
});
|
names = [names[names.length - 1]!];
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
if (names.length > 1) {
|
|
||||||
names = [names[names.length - 1]!];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
...rule,
|
...rule,
|
||||||
roleNames: names
|
roleNames: names
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
activeTagIndex={activeTagIndex}
|
/>
|
||||||
setActiveTagIndex={setActiveTagIndex}
|
) : (
|
||||||
placeholder={
|
<TagInput
|
||||||
restrictToOrgRoles
|
tags={rule.roleNames.map((name) => ({
|
||||||
? t("roleMappingAssignRoles")
|
id: name,
|
||||||
: t("roleMappingAssignRolesPlaceholderFreeform")
|
text: name
|
||||||
}
|
}))}
|
||||||
enableAutocomplete={restrictToOrgRoles}
|
setTags={(nextTags) => {
|
||||||
autocompleteOptions={roleOptions}
|
const prevRoleTags = rule.roleNames.map(
|
||||||
restrictTagsToAutocompleteOptions={restrictToOrgRoles}
|
(name) => ({
|
||||||
allowDuplicates={false}
|
id: name,
|
||||||
sortTags={true}
|
text: name
|
||||||
size="sm"
|
})
|
||||||
styleClasses={{
|
);
|
||||||
inlineTagsContainer: "min-w-0 max-w-full"
|
const next =
|
||||||
}}
|
typeof nextTags === "function"
|
||||||
/>
|
? nextTags(prevRoleTags)
|
||||||
|
: nextTags;
|
||||||
|
|
||||||
|
let names = [
|
||||||
|
...new Set(next.map((tag) => tag.text))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!supportsMultipleRolesPerUser) {
|
||||||
|
if (
|
||||||
|
names.length === 0 &&
|
||||||
|
rule.roleNames.length > 0
|
||||||
|
) {
|
||||||
|
onChange({
|
||||||
|
...rule,
|
||||||
|
roleNames: [
|
||||||
|
rule.roleNames[
|
||||||
|
rule.roleNames.length - 1
|
||||||
|
]!
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
|
names = [names[names.length - 1]!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...rule,
|
||||||
|
roleNames: names
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
activeTagIndex={activeTagIndex}
|
||||||
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
|
placeholder={t(
|
||||||
|
"roleMappingAssignRolesPlaceholderFreeform"
|
||||||
|
)}
|
||||||
|
enableAutocomplete={false}
|
||||||
|
autocompleteOptions={roleOptions}
|
||||||
|
restrictTagsToAutocompleteOptions={false}
|
||||||
|
allowDuplicates={false}
|
||||||
|
sortTags={true}
|
||||||
|
size="sm"
|
||||||
|
styleClasses={{
|
||||||
|
inlineTagsContainer: "min-w-0 max-w-full"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showFreeformRoleNamesHint && (
|
{showFreeformRoleNamesHint && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
@@ -12,13 +12,64 @@ import {
|
|||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import { Label } from "@app/components/ui/label";
|
||||||
|
import { Switch } from "@app/components/ui/switch";
|
||||||
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Destination } from "@app/components/HttpDestinationCredenza";
|
||||||
|
|
||||||
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
name: string;
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
region: string;
|
||||||
|
bucket: string;
|
||||||
|
prefix: string;
|
||||||
|
endpoint: string;
|
||||||
|
format: S3PayloadFormat;
|
||||||
|
gzip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const defaultS3Config = (): S3Config => ({
|
||||||
|
name: "",
|
||||||
|
accessKeyId: "",
|
||||||
|
secretAccessKey: "",
|
||||||
|
region: "us-east-1",
|
||||||
|
bucket: "",
|
||||||
|
prefix: "",
|
||||||
|
endpoint: "",
|
||||||
|
format: "json_array",
|
||||||
|
gzip: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export function parseS3Config(raw: string): S3Config {
|
||||||
|
try {
|
||||||
|
return { ...defaultS3Config(), ...JSON.parse(raw) };
|
||||||
|
} catch {
|
||||||
|
return defaultS3Config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface S3DestinationCredenzaProps {
|
export interface S3DestinationCredenzaProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editing: any;
|
editing: Destination | null;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
}
|
}
|
||||||
@@ -28,18 +79,84 @@ export function S3DestinationCredenza({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
editing,
|
editing,
|
||||||
orgId,
|
orgId,
|
||||||
onSaved,
|
onSaved
|
||||||
}: S3DestinationCredenzaProps) {
|
}: S3DestinationCredenzaProps) {
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [cfg, setCfg] = useState<S3Config>(defaultS3Config());
|
||||||
|
const [sendAccessLogs, setSendAccessLogs] = useState(false);
|
||||||
|
const [sendActionLogs, setSendActionLogs] = useState(false);
|
||||||
|
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
||||||
|
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setCfg(editing ? parseS3Config(editing.config) : defaultS3Config());
|
||||||
|
setSendAccessLogs(editing?.sendAccessLogs ?? false);
|
||||||
|
setSendActionLogs(editing?.sendActionLogs ?? false);
|
||||||
|
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
||||||
|
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
||||||
|
}
|
||||||
|
}, [open, editing]);
|
||||||
|
|
||||||
|
const update = (patch: Partial<S3Config>) =>
|
||||||
|
setCfg((prev) => ({ ...prev, ...patch }));
|
||||||
|
|
||||||
|
const isValid =
|
||||||
|
cfg.name.trim() !== "" &&
|
||||||
|
cfg.accessKeyId.trim() !== "" &&
|
||||||
|
cfg.secretAccessKey.trim() !== "" &&
|
||||||
|
cfg.region.trim() !== "" &&
|
||||||
|
cfg.bucket.trim() !== "";
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!isValid) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
type: "s3",
|
||||||
|
config: JSON.stringify(cfg),
|
||||||
|
sendAccessLogs,
|
||||||
|
sendActionLogs,
|
||||||
|
sendConnectionLogs,
|
||||||
|
sendRequestLogs
|
||||||
|
};
|
||||||
|
if (editing) {
|
||||||
|
await api.post(
|
||||||
|
`/org/${orgId}/event-streaming-destination/${editing.destinationId}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
toast({ title: t("s3DestUpdatedSuccess") });
|
||||||
|
} else {
|
||||||
|
await api.put(
|
||||||
|
`/org/${orgId}/event-streaming-destination`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
toast({ title: t("s3DestCreatedSuccess") });
|
||||||
|
}
|
||||||
|
onSaved();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: editing
|
||||||
|
? t("s3DestUpdateFailed")
|
||||||
|
: t("s3DestCreateFailed"),
|
||||||
|
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza open={open} onOpenChange={onOpenChange}>
|
<Credenza open={open} onOpenChange={onOpenChange}>
|
||||||
<CredenzaContent className="sm:max-w-2xl">
|
<CredenzaContent className="sm:max-w-2xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>
|
||||||
{editing
|
{editing ? t("S3DestEditTitle") : t("S3DestAddTitle")}
|
||||||
? t("S3DestEditTitle")
|
|
||||||
: t("S3DestAddTitle")}
|
|
||||||
</CredenzaTitle>
|
</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
{editing
|
{editing
|
||||||
@@ -49,13 +166,375 @@ export function S3DestinationCredenza({
|
|||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
|
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<ContactSalesBanner />
|
{editing?.lastError && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="break-words">
|
||||||
|
{editing.lastError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<HorizontalTabs
|
||||||
|
clientSide
|
||||||
|
items={[
|
||||||
|
{ title: t("s3DestTabSettings"), href: "" },
|
||||||
|
{ title: t("s3DestTabFormat"), href: "" },
|
||||||
|
{ title: t("httpDestTabLogs"), href: "" }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* ── Settings tab ────────────────────────────── */}
|
||||||
|
<div className="space-y-6 mt-4 p-1">
|
||||||
|
{/* Name */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-name">
|
||||||
|
{t("s3DestNameLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-name"
|
||||||
|
placeholder={t("s3DestNamePlaceholder")}
|
||||||
|
value={cfg.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ name: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AWS Access Key ID */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-access-key-id">
|
||||||
|
{t("s3DestAccessKeyIdLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-access-key-id"
|
||||||
|
placeholder="AKIAIOSFODNN7EXAMPLE"
|
||||||
|
value={cfg.accessKeyId}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({
|
||||||
|
accessKeyId: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AWS Secret Access Key */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-secret-key">
|
||||||
|
{t("s3DestSecretAccessKeyLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-secret-key"
|
||||||
|
type="password"
|
||||||
|
placeholder={t(
|
||||||
|
"s3DestSecretAccessKeyPlaceholder"
|
||||||
|
)}
|
||||||
|
value={cfg.secretAccessKey}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({
|
||||||
|
secretAccessKey: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Region */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-region">
|
||||||
|
{t("s3DestRegionLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-region"
|
||||||
|
placeholder="us-east-1"
|
||||||
|
value={cfg.region}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ region: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bucket */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-bucket">
|
||||||
|
{t("s3DestBucketLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-bucket"
|
||||||
|
placeholder="my-logs-bucket"
|
||||||
|
value={cfg.bucket}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ bucket: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prefix */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-prefix">
|
||||||
|
{t("s3DestPrefixLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-prefix"
|
||||||
|
placeholder="pangolin/logs"
|
||||||
|
value={cfg.prefix}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ prefix: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("s3DestPrefixDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom endpoint (optional – for S3-compatible storage) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="s3-endpoint">
|
||||||
|
{t("s3DestEndpointLabel")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="s3-endpoint"
|
||||||
|
placeholder="https://s3.example.com"
|
||||||
|
value={cfg.endpoint}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ endpoint: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("s3DestEndpointDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Format tab ───────────────────────────────── */}
|
||||||
|
<div className="space-y-6 mt-4 p-1">
|
||||||
|
{/* Gzip compression toggle */}
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Switch
|
||||||
|
id="s3-gzip"
|
||||||
|
checked={cfg.gzip}
|
||||||
|
onCheckedChange={(v) => update({ gzip: v })}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-gzip"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("s3DestGzipLabel")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t("s3DestGzipDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payload format selector */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="font-medium block">
|
||||||
|
{t("s3DestFormatTitle")}
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
|
{t("s3DestFormatDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
value={cfg.format}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
update({
|
||||||
|
format: v as S3PayloadFormat
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{/* JSON Array */}
|
||||||
|
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="json_array"
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
{t(
|
||||||
|
"httpDestFormatJsonArrayTitle"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{t(
|
||||||
|
"s3DestFormatJsonArrayDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* NDJSON */}
|
||||||
|
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="ndjson"
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
{t("httpDestFormatNdjsonTitle")}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{t(
|
||||||
|
"s3DestFormatNdjsonDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* CSV */}
|
||||||
|
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="csv"
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
{t("s3DestFormatCsvTitle")}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{t(
|
||||||
|
"s3DestFormatCsvDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Logs tab ──────────────────────────────────── */}
|
||||||
|
<div className="space-y-6 mt-4 p-1">
|
||||||
|
<div>
|
||||||
|
<label className="font-medium block">
|
||||||
|
{t("httpDestLogTypesTitle")}
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
|
{t("httpDestLogTypesDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="s3-log-access"
|
||||||
|
checked={sendAccessLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendAccessLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-log-access"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("httpDestAccessLogsTitle")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t("httpDestAccessLogsDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="s3-log-action"
|
||||||
|
checked={sendActionLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendActionLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-log-action"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("httpDestActionLogsTitle")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t("httpDestActionLogsDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="s3-log-connection"
|
||||||
|
checked={sendConnectionLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendConnectionLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-log-connection"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("httpDestConnectionLogsTitle")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t(
|
||||||
|
"httpDestConnectionLogsDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="s3-log-request"
|
||||||
|
checked={sendRequestLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendRequestLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-log-request"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("httpDestRequestLogsTitle")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t(
|
||||||
|
"httpDestRequestLogsDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HorizontalTabs>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">{t("cancel")}</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={saving}
|
||||||
|
disabled={!isValid || saving}
|
||||||
|
>
|
||||||
|
{editing
|
||||||
|
? t("s3DestSaveChanges")
|
||||||
|
: t("s3DestCreateDestination")}
|
||||||
|
</Button>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user