Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
034ff5a396 Bump node from 24-alpine to 26-alpine
Bumps node from 24-alpine to 26-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 01:34:47 +00:00
86 changed files with 671 additions and 2858 deletions

View File

@@ -1,4 +1,4 @@
FROM node:24-alpine FROM node:26-alpine
WORKDIR /app WORKDIR /app

View File

@@ -1,60 +0,0 @@
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);
}
}
};

View File

@@ -10,7 +10,6 @@ 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 { clearCertificates } from "./commands/clearCertificates";
import { disableUser2fa } from "./commands/disableUser2fa";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.scriptName("pangctl") .scriptName("pangctl")
@@ -22,6 +21,5 @@ yargs(hideBin(process.argv))
.command(deleteClient) .command(deleteClient)
.command(generateOrgCaKeys) .command(generateOrgCaKeys)
.command(clearCertificates) .command(clearCertificates)
.command(disableUser2fa)
.demandCommand() .demandCommand()
.help().argv; .help().argv;

View File

@@ -156,10 +156,6 @@
"shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката", "shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката",
"shareDeleted": "Връзката беше изтрита", "shareDeleted": "Връзката беше изтрита",
"shareDeletedDescription": "Връзката беше премахната", "shareDeletedDescription": "Връзката беше премахната",
"shareDelete": "Изтрийте споделената връзка",
"shareDeleteConfirm": "Потвърдете изтриването на споделената връзка",
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.", "shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
"accessToken": "Достъп Токен", "accessToken": "Достъп Токен",
"usageExamples": "Примери за използване", "usageExamples": "Примери за използване",
@@ -527,12 +523,6 @@
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.", "userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя", "userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
"userRemoveOrg": "Премахване на потребителя от организацията", "userRemoveOrg": "Премахване на потребителя от организацията",
"userQuestionOrgRemoveSelf": "Сигурни ли сте, че искате да премахнете себе си от тази организация?",
"userMessageOrgRemoveSelf": "Ще загубите достъп незабавно. Администратор може да ви покани отново по-късно, но ще трябва да приемете нова покана.",
"userRemoveOrgConfirmSelf": "Потвърдете премахването на себе си",
"userRemoveOrgSelf": "Премахнете себе си от организацията",
"userRemoveOrgSelfWarning": "Ще загубите достъп до тази организация незабавно.",
"userRemoveOrgConfirmPhraseSelf": "ПРЕМАХНЕТЕ МЕ ОТ ОРГАНИЗАЦИЯТА",
"users": "Потребители", "users": "Потребители",
"accessRoleMember": "Член", "accessRoleMember": "Член",
"accessRoleOwner": "Собственик", "accessRoleOwner": "Собственик",
@@ -541,11 +531,6 @@
"emailInvalid": "Невалиден имейл адрес", "emailInvalid": "Невалиден имейл адрес",
"inviteValidityDuration": "Моля, изберете продължителност", "inviteValidityDuration": "Моля, изберете продължителност",
"accessRoleSelectPlease": "Моля, изберете роля", "accessRoleSelectPlease": "Моля, изберете роля",
"removeOwnAdminRoleConfirmTitle": "Премахване на административния ви достъп?",
"removeOwnAdminRoleConfirmDescription": "След записване няма да имате повече администраторски права в тази организация. Друг администратор може да възстанови достъпа, ако е необходимо.",
"removeOwnAdminRoleConfirmButton": "Премахнете административния ми достъп",
"removeOwnAdminRoleConfirmPhrase": "ПРЕМАХНЕТЕ АДМИНИСТРАТИВНИЯ МИ ДОСТЪП",
"ownerMustRetainAdminRole": "Собственикът на организацията трябва да запази поне една администраторска роля.",
"usernameRequired": "Необходимо е потребителско име", "usernameRequired": "Необходимо е потребителско име",
"idpSelectPlease": "Моля, изберете доставчик на идентичност", "idpSelectPlease": "Моля, изберете доставчик на идентичност",
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.", "idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
@@ -630,7 +615,7 @@
"createdAt": "Създаден на", "createdAt": "Създаден на",
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.", "proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.", "proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
"proxyEnableSSL": "Активиране на TLS", "proxyEnableSSL": "Активиране на SSL",
"proxyEnableSSLDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целите.", "proxyEnableSSLDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целите.",
"target": "Цел", "target": "Цел",
"configureTarget": "Конфигуриране на цели", "configureTarget": "Конфигуриране на цели",
@@ -673,7 +658,6 @@
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.", "targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
"targetsSubmit": "Запазване на целите", "targetsSubmit": "Запазване на целите",
"addTarget": "Добавете цел", "addTarget": "Добавете цел",
"proxyMultiSiteRoundRobinNodeHelp": "Роунд Робин маршрутизирането няма да работи между сайтове, които не са свързани към един и същ възел, но автоматичното превключване ще работи.",
"targetErrorInvalidIp": "Невалиден IP адрес", "targetErrorInvalidIp": "Невалиден IP адрес",
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост", "targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
"targetErrorInvalidPort": "Невалиден порт", "targetErrorInvalidPort": "Невалиден порт",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Метод", "editInternalResourceDialogScheme": "Метод",
"editInternalResourceDialogEnableSsl": "Активирайте TLS", "editInternalResourceDialogEnableSsl": "Активирайте SSL",
"editInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.", "editInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.",
"editInternalResourceDialogDestination": "Дестинация", "editInternalResourceDialogDestination": "Дестинация",
"editInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.", "editInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Метод", "scheme": "Метод",
"createInternalResourceDialogScheme": "Метод", "createInternalResourceDialogScheme": "Метод",
"createInternalResourceDialogEnableSsl": "Активирайте TLS", "createInternalResourceDialogEnableSsl": "Активирайте SSL",
"createInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.", "createInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.",
"createInternalResourceDialogDestination": "Дестинация", "createInternalResourceDialogDestination": "Дестинация",
"createInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.", "createInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.",
@@ -2233,7 +2217,7 @@
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри", "description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
"introTitle": "Управлявано Самостоятелно-хостван Панголиин", "introTitle": "Управлявано Самостоятелно-хостван Панголиин",
"introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.", "introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.",
"introDetail": "С тази опция все още управлявате свой собствен Панголиин възел - вашите тунели, TLS терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:", "introDetail": "С тази опция все още управлявате свой собствен Панголиин възел - вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "По-прости операции", "title": "По-прости операции",
"description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало." "description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало."
@@ -2668,8 +2652,6 @@
"validPassword": "Валидна парола", "validPassword": "Валидна парола",
"validEmail": "Валиден имейл", "validEmail": "Валиден имейл",
"validSSO": "Валидно SSO", "validSSO": "Валидно SSO",
"view": "Преглед",
"configManaged": "Управлявана конфигурация",
"connectedClient": "Свързан клиент", "connectedClient": "Свързан клиент",
"resourceBlocked": "Блокирани ресурси", "resourceBlocked": "Блокирани ресурси",
"droppedByRule": "Прекратено от правило", "droppedByRule": "Прекратено от правило",
@@ -3080,7 +3062,7 @@
"streamingDatadogTitle": "Datadog", "streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.", "streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.", "streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
"streamingLastSyncError": "Възникна грешка при последната синхронизация", "streamingFailedToLoad": "Неуспешно зареждане на дестинации",
"streamingUnexpectedError": "Възникна неочаквана грешка.", "streamingUnexpectedError": "Възникна неочаквана грешка.",
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация", "streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно", "streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
@@ -3097,34 +3079,7 @@
"S3DestEditTitle": "Редактиране на дестинацията", "S3DestEditTitle": "Редактиране на дестинацията",
"S3DestAddTitle": "Добавете S3 дестинация", "S3DestAddTitle": "Добавете S3 дестинация",
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.", "S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
"S3DestAddDescription": "Конфигурирайте ново хранилище Amazon S3 (или съвместимо с S3), за да получавате събития на вашата организация.", "S3DestAddDescription": "Конфигурирайте нов крайна точка на 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 дестинация за предаване на събития.",
@@ -3136,7 +3091,7 @@
"httpDestNamePlaceholder": "Моята HTTP дестинация", "httpDestNamePlaceholder": "Моята HTTP дестинация",
"httpDestUrlLabel": "Дестинация URL", "httpDestUrlLabel": "Дестинация URL",
"httpDestUrlErrorHttpRequired": "URL адресът трябва да използва http или https", "httpDestUrlErrorHttpRequired": "URL адресът трябва да използва http или https",
"httpDestUrlErrorHttpsRequired": "HTTPS е необходимо за облачни инсталации", "httpDestUrlErrorHttpsRequired": "SSL е необходимо за облачни инсталации",
"httpDestUrlErrorInvalid": "Въведете валиден URL (напр. https://example.com/webhook)", "httpDestUrlErrorInvalid": "Въведете валиден URL (напр. https://example.com/webhook)",
"httpDestAuthTitle": "Удостоверяване", "httpDestAuthTitle": "Удостоверяване",
"httpDestAuthDescription": "Изберете как заявленията ви се удостоверяват.", "httpDestAuthDescription": "Изберете как заявленията ви се удостоверяват.",
@@ -3219,7 +3174,7 @@
"publicIpEndpoint": "Крайна точка", "publicIpEndpoint": "Крайна точка",
"lastTriggeredAt": "Последен тригер", "lastTriggeredAt": "Последен тригер",
"reject": "Отхвърляне", "reject": "Отхвърляне",
"uptimeDaysAgo": "преди {count} дни", "uptimeDaysAgo": "{count} days ago",
"uptimeToday": "Днес", "uptimeToday": "Днес",
"uptimeNoDataAvailable": "Няма налични данни", "uptimeNoDataAvailable": "Няма налични данни",
"uptimeSuffix": "време без прекъсване", "uptimeSuffix": "време без прекъсване",

View File

@@ -156,10 +156,6 @@
"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í",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Vytvořeno v", "createdAt": "Vytvořeno v",
"proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.", "proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.",
"proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.", "proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.",
"proxyEnableSSL": "Povolit TLS", "proxyEnableSSL": "Povolit SSL",
"proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená připojení HTTPS k cílům.", "proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená připojení HTTPS k cílům.",
"target": "Target", "target": "Target",
"configureTarget": "Konfigurace cílů", "configureTarget": "Konfigurace cílů",
@@ -673,7 +658,6 @@
"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",
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Došlo k chybě při poslední synchronizaci", "streamingFailedToLoad": "Nepodařilo se načíst destinace",
"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",
@@ -3097,34 +3079,7 @@
"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": "Nakonfigurujte nový Amazon S3 (nebo S3-kompatibilní) bucket, aby přijímal události vaší organizace.", "S3DestAddDescription": "Konfigurujte nový S3 koncový bod pro přijímání událostí 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í.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Erstellt am", "createdAt": "Erstellt am",
"proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.", "proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.",
"proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.", "proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.",
"proxyEnableSSL": "TLS aktivieren", "proxyEnableSSL": "SSL aktivieren",
"proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu den Zielen.", "proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu den Zielen.",
"target": "Ziel", "target": "Ziel",
"configureTarget": "Ziele konfigurieren", "configureTarget": "Ziele konfigurieren",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Schema", "editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS aktivieren", "editInternalResourceDialogEnableSsl": "SSL aktivieren",
"editInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.", "editInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.",
"editInternalResourceDialogDestination": "Ziel", "editInternalResourceDialogDestination": "Ziel",
"editInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.", "editInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Schema", "scheme": "Schema",
"createInternalResourceDialogScheme": "Schema", "createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS aktivieren", "createInternalResourceDialogEnableSsl": "SSL aktivieren",
"createInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.", "createInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.",
"createInternalResourceDialogDestination": "Ziel", "createInternalResourceDialogDestination": "Ziel",
"createInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.", "createInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.",
@@ -2233,7 +2217,7 @@
"description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen", "description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen",
"introTitle": "Verwalteter selbstgehosteter Pangolin", "introTitle": "Verwalteter selbstgehosteter Pangolin",
"introDescription": "ist eine Deployment-Option, die für Personen konzipiert wurde, die Einfachheit und zusätzliche Zuverlässigkeit wünschen, während sie ihre Daten privat und selbstgehostet halten.", "introDescription": "ist eine Deployment-Option, die für Personen konzipiert wurde, die Einfachheit und zusätzliche Zuverlässigkeit wünschen, während sie ihre Daten privat und selbstgehostet halten.",
"introDetail": "Mit dieser Option haben Sie immer noch Ihren eigenen Pangolin-Knoten Ihre Tunnel, TLS-Terminierung und Traffic bleiben auf Ihrem Server. Der Unterschied besteht darin, dass Verwaltung und Überwachung über unser Cloud-Dashboard abgewickelt werden, das eine Reihe von Vorteilen freischaltet:", "introDetail": "Mit dieser Option haben Sie immer noch Ihren eigenen Pangolin-Knoten Ihre Tunnel, SSL-Terminierung und Traffic bleiben auf Ihrem Server. Der Unterschied besteht darin, dass Verwaltung und Überwachung über unser Cloud-Dashboard abgewickelt werden, das eine Reihe von Vorteilen freischaltet:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Einfachere Operationen", "title": "Einfachere Operationen",
"description": "Sie brauchen keinen eigenen Mail-Server auszuführen oder komplexe Warnungen einzurichten. Sie erhalten Gesundheitschecks und Ausfallwarnungen aus dem Box." "description": "Sie brauchen keinen eigenen Mail-Server auszuführen oder komplexe Warnungen einzurichten. Sie erhalten Gesundheitschecks und Ausfallwarnungen aus dem Box."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Beim letzten Synchronisieren ist ein Fehler aufgetreten.", "streamingFailedToLoad": "Fehler beim Laden der Ziele",
"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",
@@ -3097,34 +3079,7 @@
"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": "Konfigurieren Sie einen neuen Amazon S3 (oder S3-kompatiblen) Bucket, um die Ereignisse Ihrer Organisation zu empfangen.", "S3DestAddDescription": "Neuen S3-Endpunkt konfigurieren, um die Ereignisse Ihrer Organisation zu erhalten.",
"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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Created At", "createdAt": "Created At",
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
"proxyEnableSSL": "Enable TLS", "proxyEnableSSL": "Enable SSL",
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the targets.", "proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the targets.",
"target": "Target", "target": "Target",
"configureTarget": "Configure Targets", "configureTarget": "Configure Targets",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Scheme", "editInternalResourceDialogScheme": "Scheme",
"editInternalResourceDialogEnableSsl": "Enable TLS", "editInternalResourceDialogEnableSsl": "Enable SSL",
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.", "editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
"editInternalResourceDialogDestination": "Destination", "editInternalResourceDialogDestination": "Destination",
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.", "editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Scheme", "scheme": "Scheme",
"createInternalResourceDialogScheme": "Scheme", "createInternalResourceDialogScheme": "Scheme",
"createInternalResourceDialogEnableSsl": "Enable TLS", "createInternalResourceDialogEnableSsl": "Enable SSL",
"createInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.", "createInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
"createInternalResourceDialogDestination": "Destination", "createInternalResourceDialogDestination": "Destination",
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.", "createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
@@ -2233,7 +2217,7 @@
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles", "description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
"introTitle": "Managed Self-Hosted Pangolin", "introTitle": "Managed Self-Hosted Pangolin",
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.", "introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
"introDetail": "With this option, you still run your own Pangolin node - your tunnels, TLS termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:", "introDetail": "With this option, you still run your own Pangolin node - your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Simpler operations", "title": "Simpler operations",
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box." "description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "An error occurred on the last sync", "streamingFailedToLoad": "Failed to load destinations",
"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",
@@ -3097,34 +3079,7 @@
"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 Amazon S3 (or S3-compatible) bucket to receive your organization's events.", "S3DestAddDescription": "Configure a new S3 endpoint 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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Creado el", "createdAt": "Creado el",
"proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.", "proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.",
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.", "proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
"proxyEnableSSL": "Activar TLS", "proxyEnableSSL": "Activar SSL",
"proxyEnableSSLDescription": "Habilita el cifrado SSL/TLS para conexiones seguras HTTPS a los objetivos.", "proxyEnableSSLDescription": "Habilita el cifrado SSL/TLS para conexiones seguras HTTPS a los objetivos.",
"target": "Target", "target": "Target",
"configureTarget": "Configurar objetivos", "configureTarget": "Configurar objetivos",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Esquema", "editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Activar TLS", "editInternalResourceDialogEnableSsl": "Activar SSL",
"editInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.", "editInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.",
"editInternalResourceDialogDestination": "Destino", "editInternalResourceDialogDestination": "Destino",
"editInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", "editInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Esquema", "scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema", "createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Activar TLS", "createInternalResourceDialogEnableSsl": "Activar SSL",
"createInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.", "createInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.",
"createInternalResourceDialogDestination": "Destino", "createInternalResourceDialogDestination": "Destino",
"createInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", "createInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.",
@@ -2233,7 +2217,7 @@
"description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra", "description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra",
"introTitle": "Pangolin autogestionado", "introTitle": "Pangolin autogestionado",
"introDescription": "es una opción de despliegue diseñada para personas que quieren simplicidad y fiabilidad extra mientras mantienen sus datos privados y autoalojados.", "introDescription": "es una opción de despliegue diseñada para personas que quieren simplicidad y fiabilidad extra mientras mantienen sus datos privados y autoalojados.",
"introDetail": "Con esta opción, todavía ejecuta su propio nodo Pangolin, sus túneles, terminación TLS y tráfico permanecen en su servidor. La diferencia es que la gestión y el control se gestionan a través de nuestro panel de control en la nube, que desbloquea una serie de ventajas:", "introDetail": "Con esta opción, todavía ejecuta su propio nodo Pangolin, sus túneles, terminación SSL y tráfico permanecen en su servidor. La diferencia es que la gestión y el control se gestionan a través de nuestro panel de control en la nube, que desbloquea una serie de ventajas:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Operaciones simples", "title": "Operaciones simples",
"description": "No necesitas ejecutar tu propio servidor de correo o configurar alertas complejas. Recibirás cheques de salud y alertas de tiempo de inactividad." "description": "No necesitas ejecutar tu propio servidor de correo o configurar alertas complejas. Recibirás cheques de salud y alertas de tiempo de inactividad."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Ocurrió un error en la última sincronización.", "streamingFailedToLoad": "Error al cargar destinos",
"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",
@@ -3097,34 +3079,7 @@
"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": "Configura un nuevo bucket de Amazon S3 (o compatible con S3) para recibir los eventos de tu organización.", "S3DestAddDescription": "Configure un nuevo punto final S3 para recibir los eventos de su 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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Créé le", "createdAt": "Créé le",
"proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.", "proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.",
"proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.", "proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.",
"proxyEnableSSL": "Activer TLS", "proxyEnableSSL": "Activer SSL",
"proxyEnableSSLDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers les cibles.", "proxyEnableSSLDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers les cibles.",
"target": "Cible", "target": "Cible",
"configureTarget": "Configurer les cibles", "configureTarget": "Configurer les cibles",
@@ -673,7 +658,6 @@
"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",
@@ -1372,7 +1356,7 @@
"sidebarSites": "Nœuds", "sidebarSites": "Nœuds",
"sidebarApprovals": "Demandes d'approbation", "sidebarApprovals": "Demandes d'approbation",
"sidebarResources": "Ressource", "sidebarResources": "Ressource",
"sidebarProxyResources": "Publique", "sidebarProxyResources": "Publiques",
"sidebarClientResources": "Privé", "sidebarClientResources": "Privé",
"sidebarAccessControl": "Contrôle d'accès", "sidebarAccessControl": "Contrôle d'accès",
"sidebarLogsAndAnalytics": "Journaux & Analytiques", "sidebarLogsAndAnalytics": "Journaux & Analytiques",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Méthode HTTP", "editInternalResourceDialogScheme": "Méthode HTTP",
"editInternalResourceDialogEnableSsl": "Activer TLS", "editInternalResourceDialogEnableSsl": "Activer SSL",
"editInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.", "editInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.",
"editInternalResourceDialogDestination": "Destination", "editInternalResourceDialogDestination": "Destination",
"editInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", "editInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Méthode HTTP", "scheme": "Méthode HTTP",
"createInternalResourceDialogScheme": "Méthode HTTP", "createInternalResourceDialogScheme": "Méthode HTTP",
"createInternalResourceDialogEnableSsl": "Activer TLS", "createInternalResourceDialogEnableSsl": "Activer SSL",
"createInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.", "createInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.",
"createInternalResourceDialogDestination": "Destination", "createInternalResourceDialogDestination": "Destination",
"createInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", "createInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.",
@@ -2233,7 +2217,7 @@
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires", "description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
"introTitle": "Pangolin auto-hébergé géré", "introTitle": "Pangolin auto-hébergé géré",
"introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.", "introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.",
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin - vos tunnels, la terminaison TLS et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :", "introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin - vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Opérations plus simples", "title": "Opérations plus simples",
"description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite." "description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite."
@@ -2474,8 +2458,8 @@
"manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources", "manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources",
"downloadClientBannerTitle": "Télécharger le client Pangolin", "downloadClientBannerTitle": "Télécharger le client Pangolin",
"downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.", "downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.",
"manageMachineClients": "Gérer les clients de la machine", "manageMachineClients": "Gérer les machines",
"manageMachineClientsDescription": "Créer et gérer des clients que les serveurs et les systèmes utilisent pour se connecter en privé aux ressources", "manageMachineClientsDescription": "Créer et gérer les clients que les serveurs et systèmes utilisent pour se connecter en privé aux ressources",
"machineClientsBannerTitle": "Serveurs & Systèmes automatisés", "machineClientsBannerTitle": "Serveurs & Systèmes automatisés",
"machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.", "machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.",
"machineClientsBannerPangolinCLI": "Pangolin CLI", "machineClientsBannerPangolinCLI": "Pangolin CLI",
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Une erreur s'est produite lors de la dernière synchronisation", "streamingFailedToLoad": "Impossible de charger les destinations",
"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",
@@ -3097,34 +3079,7 @@
"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": "Configurez un nouveau bucket Amazon S3 (ou compatible S3) pour recevoir les événements de votre organisation.", "S3DestAddDescription": "Configurer un nouveau point de terminaison 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.",
@@ -3199,6 +3154,7 @@
"healthCheckTabAdvanced": "Avancé", "healthCheckTabAdvanced": "Avancé",
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.", "healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
"uptime30d": "Disponibilité (30j)", "uptime30d": "Disponibilité (30j)",
"uptimeNoData": "Aucune donnée",
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité", "idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
"idpAddActionImportFromOrg": "Importer d'une autre organisation", "idpAddActionImportFromOrg": "Importer d'une autre organisation",
"idpImportDialogTitle": "Importer le fournisseur d'identité", "idpImportDialogTitle": "Importer le fournisseur d'identité",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Creato Il", "createdAt": "Creato Il",
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.", "proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.", "proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
"proxyEnableSSL": "Abilita TLS", "proxyEnableSSL": "Abilita SSL",
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.", "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.",
"target": "Target", "target": "Target",
"configureTarget": "Configura Risorse Interne", "configureTarget": "Configura Risorse Interne",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Metodo HTTP", "editInternalResourceDialogScheme": "Metodo HTTP",
"editInternalResourceDialogEnableSsl": "Abilitare TLS", "editInternalResourceDialogEnableSsl": "Abilitare SSL",
"editInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.", "editInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.",
"editInternalResourceDialogDestination": "Destinazione", "editInternalResourceDialogDestination": "Destinazione",
"editInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.", "editInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Metodo HTTP", "scheme": "Metodo HTTP",
"createInternalResourceDialogScheme": "Metodo HTTP", "createInternalResourceDialogScheme": "Metodo HTTP",
"createInternalResourceDialogEnableSsl": "Abilitare TLS", "createInternalResourceDialogEnableSsl": "Abilitare SSL",
"createInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.", "createInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.",
"createInternalResourceDialogDestination": "Destinazione", "createInternalResourceDialogDestination": "Destinazione",
"createInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.", "createInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.",
@@ -2233,7 +2217,7 @@
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra", "description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
"introTitle": "Managed Self-Hosted Pangolin", "introTitle": "Managed Self-Hosted Pangolin",
"introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.", "introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.",
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin - i tunnel, la terminazione TLS e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:", "introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin - i tunnel, la terminazione SSL e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Operazioni più semplici", "title": "Operazioni più semplici",
"description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella." "description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Si è verificato un errore durante l'ultima sincronizzazione", "streamingFailedToLoad": "Impossibile caricare le destinazioni",
"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",
@@ -3097,34 +3079,7 @@
"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 bucket Amazon S3 (o compatibile con S3) per ricevere gli eventi della tua organizzazione.", "S3DestAddDescription": "Configura un nuovo endpoint 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.",

View File

@@ -156,10 +156,6 @@
"shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.", "shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.",
"shareDeleted": "링크가 삭제되었습니다.", "shareDeleted": "링크가 삭제되었습니다.",
"shareDeletedDescription": "링크가 삭제되었습니다.", "shareDeletedDescription": "링크가 삭제되었습니다.",
"shareDelete": "공유 링크 삭제",
"shareDeleteConfirm": "공유 링크 삭제 확인",
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.", "shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
"accessToken": "액세스 토큰", "accessToken": "액세스 토큰",
"usageExamples": "사용 예", "usageExamples": "사용 예",
@@ -527,12 +523,6 @@
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.", "userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
"userRemoveOrgConfirm": "사용자 제거 확인", "userRemoveOrgConfirm": "사용자 제거 확인",
"userRemoveOrg": "조직에서 사용자 제거", "userRemoveOrg": "조직에서 사용자 제거",
"userQuestionOrgRemoveSelf": "이 조직에서 자신을 제거하시겠습니까?",
"userMessageOrgRemoveSelf": "귀하는 즉시 접근 권한을 잃게 됩니다. 관리자가 나중에 다시 초대할 수 있지만, 새 초대를 수락해야 합니다.",
"userRemoveOrgConfirmSelf": "내 제거 확인",
"userRemoveOrgSelf": "조직에서 자신을 제거하십시오",
"userRemoveOrgSelfWarning": "귀하는 이 조직에 대한 접근 권한을 즉시 상실합니다.",
"userRemoveOrgConfirmPhraseSelf": "조직에서 나를 제거",
"users": "사용자", "users": "사용자",
"accessRoleMember": "회원", "accessRoleMember": "회원",
"accessRoleOwner": "소유자", "accessRoleOwner": "소유자",
@@ -541,11 +531,6 @@
"emailInvalid": "유효하지 않은 이메일 주소입니다.", "emailInvalid": "유효하지 않은 이메일 주소입니다.",
"inviteValidityDuration": "지속 시간을 선택하십시오.", "inviteValidityDuration": "지속 시간을 선택하십시오.",
"accessRoleSelectPlease": "역할을 선택하세요", "accessRoleSelectPlease": "역할을 선택하세요",
"removeOwnAdminRoleConfirmTitle": "관리자 권한을 제거하시겠습니까?",
"removeOwnAdminRoleConfirmDescription": "저장 후 이 조직에 대한 관리자 권한이 없어집니다. 필요한 경우 다른 관리자가 접근 권한을 복구할 수 있습니다.",
"removeOwnAdminRoleConfirmButton": "내 관리자 권한 제거",
"removeOwnAdminRoleConfirmPhrase": "내 관리자 권한 제거",
"ownerMustRetainAdminRole": "조직 소유자는 최소한 하나의 관리자 역할을 유지해야 합니다.",
"usernameRequired": "사용자 이름은 필수입니다.", "usernameRequired": "사용자 이름은 필수입니다.",
"idpSelectPlease": "신원 제공자를 선택하십시오", "idpSelectPlease": "신원 제공자를 선택하십시오",
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.", "idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
@@ -630,7 +615,7 @@
"createdAt": "생성일", "createdAt": "생성일",
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
"proxyEnableSSL": "TLS 활성화", "proxyEnableSSL": "SSL 활성화",
"proxyEnableSSLDescription": "타겟과의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화를 활성화하세요.", "proxyEnableSSLDescription": "타겟과의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화를 활성화하세요.",
"target": "대상", "target": "대상",
"configureTarget": "대상 구성", "configureTarget": "대상 구성",
@@ -673,7 +658,6 @@
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.", "targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
"targetsSubmit": "대상 저장", "targetsSubmit": "대상 저장",
"addTarget": "대상 추가", "addTarget": "대상 추가",
"proxyMultiSiteRoundRobinNodeHelp": "라운드 로빈 라우팅은 동일한 노드에 연결되지 않은 사이트 간에는 작동하지 않으나, 대체 라우팅은 작동합니다.",
"targetErrorInvalidIp": "유효하지 않은 IP 주소", "targetErrorInvalidIp": "유효하지 않은 IP 주소",
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.", "targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
"targetErrorInvalidPort": "유효하지 않은 포트", "targetErrorInvalidPort": "유효하지 않은 포트",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "스킴", "editInternalResourceDialogScheme": "스킴",
"editInternalResourceDialogEnableSsl": "TLS 활성화", "editInternalResourceDialogEnableSsl": "SSL 활성화",
"editInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.", "editInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.",
"editInternalResourceDialogDestination": "대상지", "editInternalResourceDialogDestination": "대상지",
"editInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.", "editInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "스킴", "scheme": "스킴",
"createInternalResourceDialogScheme": "스킴", "createInternalResourceDialogScheme": "스킴",
"createInternalResourceDialogEnableSsl": "TLS 활성화", "createInternalResourceDialogEnableSsl": "SSL 활성화",
"createInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.", "createInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.",
"createInternalResourceDialogDestination": "대상지", "createInternalResourceDialogDestination": "대상지",
"createInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.", "createInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.",
@@ -2233,7 +2217,7 @@
"description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함", "description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함",
"introTitle": "관리 자체 호스팅 팡골린", "introTitle": "관리 자체 호스팅 팡골린",
"introDescription": "는 자신의 데이터를 프라이빗하고 자체 호스팅을 유지하면서 더 간단하고 추가적인 신뢰성을 원하는 사람들을 위한 배포 옵션입니다.", "introDescription": "는 자신의 데이터를 프라이빗하고 자체 호스팅을 유지하면서 더 간단하고 추가적인 신뢰성을 원하는 사람들을 위한 배포 옵션입니다.",
"introDetail": "이 옵션을 사용하면 여전히 자신의 팡골린 노드를 운영하고 - 터널, TLS 종료 및 트래픽 모두 서버에 유지됩니다. 차이점은 관리 및 모니터링이 클라우드 대시보드를 통해 처리되어 여러 혜택을 제공합니다.", "introDetail": "이 옵션을 사용하면 여전히 자신의 팡골린 노드를 운영하고 - 터널, SSL 종료 및 트래픽 모두 서버에 유지됩니다. 차이점은 관리 및 모니터링이 클라우드 대시보드를 통해 처리되어 여러 혜택을 제공합니다.",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "더 간단한 운영", "title": "더 간단한 운영",
"description": "자체 메일 서버를 운영하거나 복잡한 경고를 설정할 필요가 없습니다. 기본적으로 상태 점검 및 다운타임 경고를 받을 수 있습니다." "description": "자체 메일 서버를 운영하거나 복잡한 경고를 설정할 필요가 없습니다. 기본적으로 상태 점검 및 다운타임 경고를 받을 수 있습니다."
@@ -2668,8 +2652,6 @@
"validPassword": "유효한 비밀번호", "validPassword": "유효한 비밀번호",
"validEmail": "유효한 이메일", "validEmail": "유효한 이메일",
"validSSO": "유효한 SSO", "validSSO": "유효한 SSO",
"view": "보기",
"configManaged": "구성 관리됨",
"connectedClient": "연결된 클라이언트", "connectedClient": "연결된 클라이언트",
"resourceBlocked": "리소스 차단됨", "resourceBlocked": "리소스 차단됨",
"droppedByRule": "룰에 의해 드롭됨", "droppedByRule": "룰에 의해 드롭됨",
@@ -3080,7 +3062,7 @@
"streamingDatadogTitle": "데이터독", "streamingDatadogTitle": "데이터독",
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.", "streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.", "streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
"streamingLastSyncError": "마지막 동기화에서 오류가 발생했습니다.", "streamingFailedToLoad": "대상 로드에 실패했습니다",
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.", "streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다", "streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다", "streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
@@ -3097,34 +3079,7 @@
"S3DestEditTitle": "대상지 수정", "S3DestEditTitle": "대상지 수정",
"S3DestAddTitle": "S3 대상지 추가", "S3DestAddTitle": "S3 대상지 추가",
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.", "S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
"S3DestAddDescription": "조직의 이벤트를 수신할 새로운 Amazon S3(또는 S3 호환) 버킷을 구성하세요.", "S3DestAddDescription": "조직의 이벤트를 받기 위한 새로운 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 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Opprettet", "createdAt": "Opprettet",
"proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.",
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
"proxyEnableSSL": "Aktiver TLS", "proxyEnableSSL": "Aktiver SSL",
"proxyEnableSSLDescription": "Aktivere SSL/TLS-kryptering for sikker HTTPS tilkobling til målene.", "proxyEnableSSLDescription": "Aktivere SSL/TLS-kryptering for sikker HTTPS tilkobling til målene.",
"target": "Target", "target": "Target",
"configureTarget": "Konfigurer mål", "configureTarget": "Konfigurer mål",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Skjema", "editInternalResourceDialogScheme": "Skjema",
"editInternalResourceDialogEnableSsl": "Aktiver TLS", "editInternalResourceDialogEnableSsl": "Aktiver SSL",
"editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.", "editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
"editInternalResourceDialogDestination": "Destinasjon", "editInternalResourceDialogDestination": "Destinasjon",
"editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", "editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Skjema", "scheme": "Skjema",
"createInternalResourceDialogScheme": "Skjema", "createInternalResourceDialogScheme": "Skjema",
"createInternalResourceDialogEnableSsl": "Aktiver TLS", "createInternalResourceDialogEnableSsl": "Aktiver SSL",
"createInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.", "createInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
"createInternalResourceDialogDestination": "Destinasjon", "createInternalResourceDialogDestination": "Destinasjon",
"createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", "createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
@@ -2233,7 +2217,7 @@
"description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell", "description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell",
"introTitle": "Administrert Self-Hosted Pangolin", "introTitle": "Administrert Self-Hosted Pangolin",
"introDescription": "er et alternativ for bruk utviklet for personer som ønsker enkel og ekstra pålitelighet mens de fortsatt holder sine data privat og selvdrevne.", "introDescription": "er et alternativ for bruk utviklet for personer som ønsker enkel og ekstra pålitelighet mens de fortsatt holder sine data privat og selvdrevne.",
"introDetail": "Med dette valget kjører du fortsatt din egen Pangolin-node - tunneler, TLS-terminering og trafikken ligger på serveren din. Forskjellen er at behandling og overvåking håndteres gjennom vårt skydashbord, som låser opp en rekke fordeler:", "introDetail": "Med dette valget kjører du fortsatt din egen Pangolin-node - tunneler, SSL-terminering og trafikken ligger på serveren din. Forskjellen er at behandling og overvåking håndteres gjennom vårt skydashbord, som låser opp en rekke fordeler:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Enklere operasjoner", "title": "Enklere operasjoner",
"description": "Ingen grunn til å kjøre din egen e-postserver eller sette opp kompleks varsling. Du vil få helsesjekk og nedetid varsler ut av boksen." "description": "Ingen grunn til å kjøre din egen e-postserver eller sette opp kompleks varsling. Du vil få helsesjekk og nedetid varsler ut av boksen."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Det oppstod en feil under siste synkronisering", "streamingFailedToLoad": "Kan ikke laste inn destinasjoner",
"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",
@@ -3097,34 +3079,7 @@
"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 en ny Amazon S3 (eller S3-kompatibel) bucket for å motta din organisasjons hendelser.", "S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens 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.",
@@ -3219,7 +3174,7 @@
"publicIpEndpoint": "Endepunkt", "publicIpEndpoint": "Endepunkt",
"lastTriggeredAt": "Siste utløste", "lastTriggeredAt": "Siste utløste",
"reject": "Avvis", "reject": "Avvis",
"uptimeDaysAgo": "{count} dager siden", "uptimeDaysAgo": "{count} days ago",
"uptimeToday": "I dag", "uptimeToday": "I dag",
"uptimeNoDataAvailable": "Ingen data tilgjengelig", "uptimeNoDataAvailable": "Ingen data tilgjengelig",
"uptimeSuffix": "oppetid", "uptimeSuffix": "oppetid",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Aangemaakt op", "createdAt": "Aangemaakt op",
"proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.", "proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.",
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.", "proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
"proxyEnableSSL": "TLS inschakelen", "proxyEnableSSL": "SSL inschakelen",
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar de doelen.", "proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar de doelen.",
"target": "Target", "target": "Target",
"configureTarget": "Doelstellingen configureren", "configureTarget": "Doelstellingen configureren",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Schema", "editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS inschakelen", "editInternalResourceDialogEnableSsl": "SSL inschakelen",
"editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.", "editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
"editInternalResourceDialogDestination": "Bestemming", "editInternalResourceDialogDestination": "Bestemming",
"editInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", "editInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Schema", "scheme": "Schema",
"createInternalResourceDialogScheme": "Schema", "createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS inschakelen", "createInternalResourceDialogEnableSsl": "SSL inschakelen",
"createInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.", "createInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
"createInternalResourceDialogDestination": "Bestemming", "createInternalResourceDialogDestination": "Bestemming",
"createInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", "createInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.",
@@ -2233,7 +2217,7 @@
"description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders", "description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders",
"introTitle": "Beheerde zelfgehoste pangolin", "introTitle": "Beheerde zelfgehoste pangolin",
"introDescription": "is een implementatieoptie ontworpen voor mensen die eenvoud en extra betrouwbaarheid willen, terwijl hun gegevens privé en zelf georganiseerd blijven.", "introDescription": "is een implementatieoptie ontworpen voor mensen die eenvoud en extra betrouwbaarheid willen, terwijl hun gegevens privé en zelf georganiseerd blijven.",
"introDetail": "Met deze optie beheert u nog steeds uw eigen Pangolin node - uw tunnels, TLS-verbinding en verkeer alles op uw server. Het verschil is dat beheer en monitoring worden behandeld via onze cloud dashboard, wat een aantal voordelen oplevert:", "introDetail": "Met deze optie beheert u nog steeds uw eigen Pangolin node - uw tunnels, SSL-verbinding en verkeer alles op uw server. Het verschil is dat beheer en monitoring worden behandeld via onze cloud dashboard, wat een aantal voordelen oplevert:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Simpler operaties", "title": "Simpler operaties",
"description": "Je hoeft geen eigen mailserver te draaien of complexe waarschuwingen in te stellen. Je krijgt gezondheidscontroles en downtime meldingen uit de box." "description": "Je hoeft geen eigen mailserver te draaien of complexe waarschuwingen in te stellen. Je krijgt gezondheidscontroles en downtime meldingen uit de box."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Er is een fout opgetreden bij de laatste synchronisatie", "streamingFailedToLoad": "Laden van bestemmingen mislukt",
"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",
@@ -3097,34 +3079,7 @@
"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 nieuwe Amazon S3 (of S3-compatibele) bucket om de gebeurtenissen van uw organisatie te ontvangen.", "S3DestAddDescription": "Configureer een nieuw S3-eindpunt 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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Utworzono", "createdAt": "Utworzono",
"proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.", "proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.",
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.", "proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
"proxyEnableSSL": "Włącz TLS", "proxyEnableSSL": "Włącz SSL",
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z celami.", "proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z celami.",
"target": "Target", "target": "Target",
"configureTarget": "Konfiguruj Targety", "configureTarget": "Konfiguruj Targety",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Schemat", "editInternalResourceDialogScheme": "Schemat",
"editInternalResourceDialogEnableSsl": "Włącz TLS", "editInternalResourceDialogEnableSsl": "Włącz SSL",
"editInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.", "editInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.",
"editInternalResourceDialogDestination": "Miejsce docelowe", "editInternalResourceDialogDestination": "Miejsce docelowe",
"editInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", "editInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Schemat", "scheme": "Schemat",
"createInternalResourceDialogScheme": "Schemat", "createInternalResourceDialogScheme": "Schemat",
"createInternalResourceDialogEnableSsl": "Włącz TLS", "createInternalResourceDialogEnableSsl": "Włącz SSL",
"createInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.", "createInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.",
"createInternalResourceDialogDestination": "Miejsce docelowe", "createInternalResourceDialogDestination": "Miejsce docelowe",
"createInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", "createInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
@@ -2233,7 +2217,7 @@
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami", "description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
"introTitle": "Zarządzany samowystarczalny Pangolin", "introTitle": "Zarządzany samowystarczalny Pangolin",
"introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.", "introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.",
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin - tunele, zakończenie TLS i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:", "introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin - tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Uproszczone operacje", "title": "Uproszczone operacje",
"description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju." "description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju."
@@ -2668,8 +2652,6 @@
"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łę",
@@ -3080,7 +3062,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ąć.",
"streamingLastSyncError": "Wystąpił błąd podczas ostatniej synchronizacji", "streamingFailedToLoad": "Nie udało się załadować miejsc docelowych",
"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",
@@ -3097,34 +3079,7 @@
"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 zasobnik Amazon S3 (lub zgodny z S3), aby otrzymywać zdarzenia twojej organizacji.", "S3DestAddDescription": "Skonfiguruj nowy punkt końcowy S3, aby odbierać 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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Criado Em", "createdAt": "Criado Em",
"proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.", "proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.",
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.", "proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
"proxyEnableSSL": "Habilitar TLS", "proxyEnableSSL": "Habilitar SSL",
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras aos alvos.", "proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras aos alvos.",
"target": "Target", "target": "Target",
"configureTarget": "Configurar Alvos", "configureTarget": "Configurar Alvos",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Esquema", "editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Ativar TLS", "editInternalResourceDialogEnableSsl": "Ativar SSL",
"editInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.", "editInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.",
"editInternalResourceDialogDestination": "Destino", "editInternalResourceDialogDestination": "Destino",
"editInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.", "editInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Esquema", "scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema", "createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Ativar TLS", "createInternalResourceDialogEnableSsl": "Ativar SSL",
"createInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.", "createInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.",
"createInternalResourceDialogDestination": "Destino", "createInternalResourceDialogDestination": "Destino",
"createInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.", "createInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.",
@@ -2233,7 +2217,7 @@
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos", "description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
"introTitle": "Pangolin Auto-Hospedado Gerenciado", "introTitle": "Pangolin Auto-Hospedado Gerenciado",
"introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.", "introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.",
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin - seus túneis, terminação TLS e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:", "introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin - seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Operações simples", "title": "Operações simples",
"description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade." "description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade."
@@ -2668,8 +2652,6 @@
"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",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Ocorreu um erro na última sincronização", "streamingFailedToLoad": "Falha ao carregar destinos",
"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",
@@ -3097,34 +3079,7 @@
"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 bucket Amazon S3 (ou compatível com S3) para receber os eventos da sua organização.", "S3DestAddDescription": "Configure um novo endpoint 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.",

View File

@@ -156,10 +156,6 @@
"shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки", "shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки",
"shareDeleted": "Ссылка удалена", "shareDeleted": "Ссылка удалена",
"shareDeletedDescription": "Ссылка была успешно удалена", "shareDeletedDescription": "Ссылка была успешно удалена",
"shareDelete": "Удалить общую ссылку",
"shareDeleteConfirm": "Подтвердите удаление общей ссылки",
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.", "shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
"accessToken": "Токен доступа", "accessToken": "Токен доступа",
"usageExamples": "Примеры использования", "usageExamples": "Примеры использования",
@@ -527,12 +523,6 @@
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.", "userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
"userRemoveOrgConfirm": "Подтвердить удаление пользователя", "userRemoveOrgConfirm": "Подтвердить удаление пользователя",
"userRemoveOrg": "Удалить пользователя из организации", "userRemoveOrg": "Удалить пользователя из организации",
"userQuestionOrgRemoveSelf": "Вы уверены, что хотите удалить себя из этой организации?",
"userMessageOrgRemoveSelf": "Вы немедленно потеряете доступ. Администратор сможет снова пригласить вас позже, но вам нужно будет принять новое приглашение.",
"userRemoveOrgConfirmSelf": "Подтвердите удаление себя",
"userRemoveOrgSelf": "Удалите себя из организации",
"userRemoveOrgSelfWarning": "Вы немедленно потеряете доступ к этой организации.",
"userRemoveOrgConfirmPhraseSelf": "Удалить себя из организации",
"users": "Пользователи", "users": "Пользователи",
"accessRoleMember": "Участник", "accessRoleMember": "Участник",
"accessRoleOwner": "Владелец", "accessRoleOwner": "Владелец",
@@ -541,11 +531,6 @@
"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.",
@@ -630,7 +615,7 @@
"createdAt": "Создано в", "createdAt": "Создано в",
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.", "proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.", "proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
"proxyEnableSSL": "Включить TLS", "proxyEnableSSL": "Включить SSL",
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS соединений с целями.", "proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS соединений с целями.",
"target": "Target", "target": "Target",
"configureTarget": "Настроить адресаты", "configureTarget": "Настроить адресаты",
@@ -673,7 +658,6 @@
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.", "targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
"targetsSubmit": "Сохранить цели", "targetsSubmit": "Сохранить цели",
"addTarget": "Добавить цель", "addTarget": "Добавить цель",
"proxyMultiSiteRoundRobinNodeHelp": "Роутинг с балансировкой нагрузки не будет работать между сайтами, не подключенными к одному и тому же узлу, но подмена будет работать.",
"targetErrorInvalidIp": "Неверный IP-адрес", "targetErrorInvalidIp": "Неверный IP-адрес",
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста", "targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
"targetErrorInvalidPort": "Неверный порт", "targetErrorInvalidPort": "Неверный порт",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Схема", "editInternalResourceDialogScheme": "Схема",
"editInternalResourceDialogEnableSsl": "Включить TLS", "editInternalResourceDialogEnableSsl": "Включить SSL",
"editInternalResourceDialogEnableSslDescription": "Включите шифрование SSL/TLS для защищенных HTTPS соединений с конечной точкой.", "editInternalResourceDialogEnableSslDescription": "Включите шифрование SSL/TLS для защищенных HTTPS соединений с конечной точкой.",
"editInternalResourceDialogDestination": "Пункт назначения", "editInternalResourceDialogDestination": "Пункт назначения",
"editInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.", "editInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Схема", "scheme": "Схема",
"createInternalResourceDialogScheme": "Схема", "createInternalResourceDialogScheme": "Схема",
"createInternalResourceDialogEnableSsl": "Включить TLS", "createInternalResourceDialogEnableSsl": "Включить SSL",
"createInternalResourceDialogEnableSslDescription": "Включите SSL/TLS шифрование для защищенных HTTPS соединений с конечной точкой.", "createInternalResourceDialogEnableSslDescription": "Включите SSL/TLS шифрование для защищенных HTTPS соединений с конечной точкой.",
"createInternalResourceDialogDestination": "Пункт назначения", "createInternalResourceDialogDestination": "Пункт назначения",
"createInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.", "createInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.",
@@ -2233,7 +2217,7 @@
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками", "description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
"introTitle": "Управляемый Само-Хост Панголина", "introTitle": "Управляемый Само-Хост Панголина",
"introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.", "introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.",
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin - туннели, TLS, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:", "introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin - туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Более простые операции", "title": "Более простые операции",
"description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки." "description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки."
@@ -2668,8 +2652,6 @@
"validPassword": "Допустимый пароль", "validPassword": "Допустимый пароль",
"validEmail": "Valid email", "validEmail": "Valid email",
"validSSO": "Valid SSO", "validSSO": "Valid SSO",
"view": "Просмотр",
"configManaged": "Конфигурация управляется",
"connectedClient": "Подключенный клиент", "connectedClient": "Подключенный клиент",
"resourceBlocked": "Ресурс заблокирован", "resourceBlocked": "Ресурс заблокирован",
"droppedByRule": "Отброшено по правилам", "droppedByRule": "Отброшено по правилам",
@@ -3080,7 +3062,7 @@
"streamingDatadogTitle": "Datadog", "streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.", "streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.", "streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
"streamingLastSyncError": "Во время последней синхронизации произошла ошибка", "streamingFailedToLoad": "Не удалось загрузить места назначения",
"streamingUnexpectedError": "Произошла непредвиденная ошибка.", "streamingUnexpectedError": "Произошла непредвиденная ошибка.",
"streamingFailedToUpdate": "Не удалось обновить место назначения", "streamingFailedToUpdate": "Не удалось обновить место назначения",
"streamingDeletedSuccess": "Адрес назначения успешно удален", "streamingDeletedSuccess": "Адрес назначения успешно удален",
@@ -3097,34 +3079,7 @@
"S3DestEditTitle": "Редактировать пункт назначения", "S3DestEditTitle": "Редактировать пункт назначения",
"S3DestAddTitle": "Добавить S3 пункт назначения", "S3DestAddTitle": "Добавить S3 пункт назначения",
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.", "S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
"S3DestAddDescription": "Настройте новый Amazon S3 (или совместимое S3) хранилище для получения событий вашей организации.", "S3DestAddDescription": "Настройте новую 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.",

View File

@@ -156,10 +156,6 @@
"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",
@@ -527,12 +523,6 @@
"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",
@@ -541,11 +531,6 @@
"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ı.",
@@ -630,7 +615,7 @@
"createdAt": "Oluşturulma Tarihi", "createdAt": "Oluşturulma Tarihi",
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.", "proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.", "proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
"proxyEnableSSL": "TLS Etkinleştir", "proxyEnableSSL": "SSL Etkinleştir",
"proxyEnableSSLDescription": "Hedeflere güvenli HTTPS bağlantıları için SSL/TLS şifrelemesini etkinleştirin.", "proxyEnableSSLDescription": "Hedeflere güvenli HTTPS bağlantıları için SSL/TLS şifrelemesini etkinleştirin.",
"target": "Hedef", "target": "Hedef",
"configureTarget": "Hedefleri Yapılandır", "configureTarget": "Hedefleri Yapılandır",
@@ -673,7 +658,6 @@
"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",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Şema", "editInternalResourceDialogScheme": "Şema",
"editInternalResourceDialogEnableSsl": "TLS Etkinleştir", "editInternalResourceDialogEnableSsl": "SSL'i Etkinleştir",
"editInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.", "editInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.",
"editInternalResourceDialogDestination": "Hedef", "editInternalResourceDialogDestination": "Hedef",
"editInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.", "editInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Şema", "scheme": "Şema",
"createInternalResourceDialogScheme": "Şema", "createInternalResourceDialogScheme": "Şema",
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir", "createInternalResourceDialogEnableSsl": "SSL'i Etkinleştir",
"createInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.", "createInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.",
"createInternalResourceDialogDestination": "Hedef", "createInternalResourceDialogDestination": "Hedef",
"createInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.", "createInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.",
@@ -2233,7 +2217,7 @@
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu", "description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin", "introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.", "introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz - tünelleriniz, TLS bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:", "introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz - tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "Daha basit işlemler", "title": "Daha basit işlemler",
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız." "description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
@@ -2668,8 +2652,6 @@
"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ı",
@@ -3080,7 +3062,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.",
"streamingLastSyncError": "Son senkronizasyonda bir hata oluştu", "streamingFailedToLoad": "Hedefler yüklenemedi",
"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",
@@ -3097,34 +3079,7 @@
"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 etkinliklerini almak için yeni bir Amazon S3 (veya S3-uyumlu) kovası yapılandırın.", "S3DestAddDescription": "Kuruluşunuzun olaylarını almak için yeni bir S3 uç noktası 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.",

View File

@@ -32,7 +32,7 @@
"trialActive": "免费试用中", "trialActive": "免费试用中",
"trialExpired": "试用到期", "trialExpired": "试用到期",
"trialHasEnded": "您的试用已结束。", "trialHasEnded": "您的试用已结束。",
"trialDaysRemaining": "{count, plural, other {# 天剩余}}", "trialDaysRemaining": "{count, plural, one {# day remaining} other {# days remaining}}",
"trialDaysLeftShort": "试用期剩余 {days} 天", "trialDaysLeftShort": "试用期剩余 {days} 天",
"trialGoToBilling": "转到账单页面", "trialGoToBilling": "转到账单页面",
"subscriptionViolationViewBilling": "查看计费", "subscriptionViolationViewBilling": "查看计费",
@@ -156,10 +156,6 @@
"shareErrorDeleteMessage": "删除链接时出错", "shareErrorDeleteMessage": "删除链接时出错",
"shareDeleted": "链接已删除", "shareDeleted": "链接已删除",
"shareDeletedDescription": "链接已删除", "shareDeletedDescription": "链接已删除",
"shareDelete": "删除共享链接",
"shareDeleteConfirm": "确认删除共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。", "shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
"accessToken": "访问令牌", "accessToken": "访问令牌",
"usageExamples": "用法示例", "usageExamples": "用法示例",
@@ -307,7 +303,7 @@
"accessUserManage": "管理用户", "accessUserManage": "管理用户",
"accessUsersDescription": "邀请和管理访问此组织的用户", "accessUsersDescription": "邀请和管理访问此组织的用户",
"accessUsersSearch": "搜索用户...", "accessUsersSearch": "搜索用户...",
"accessUsersRoleFilterCount": "{count, plural, other {# 角色}}", "accessUsersRoleFilterCount": "{count, plural, one {# role} other {# roles}}",
"accessUsersRoleFilterClear": "清除角色过滤器", "accessUsersRoleFilterClear": "清除角色过滤器",
"accessUserCreate": "创建用户", "accessUserCreate": "创建用户",
"accessUserRemove": "删除用户", "accessUserRemove": "删除用户",
@@ -527,12 +523,6 @@
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。", "userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
"userRemoveOrgConfirm": "确认删除用户", "userRemoveOrgConfirm": "确认删除用户",
"userRemoveOrg": "从组织中删除用户", "userRemoveOrg": "从组织中删除用户",
"userQuestionOrgRemoveSelf": "你确定要将自己从这个组织中移除吗?",
"userMessageOrgRemoveSelf": "你将立即失去访问权限。管理员稍后可以再次邀请你,但你需要接受新的邀请。",
"userRemoveOrgConfirmSelf": "确认删除我自己",
"userRemoveOrgSelf": "将自己从组织中移除",
"userRemoveOrgSelfWarning": "你将立即失去对此组织的访问权限。",
"userRemoveOrgConfirmPhraseSelf": "从组织中移除我自己",
"users": "用户", "users": "用户",
"accessRoleMember": "成员", "accessRoleMember": "成员",
"accessRoleOwner": "所有者", "accessRoleOwner": "所有者",
@@ -541,11 +531,6 @@
"emailInvalid": "无效的电子邮件地址", "emailInvalid": "无效的电子邮件地址",
"inviteValidityDuration": "请选择持续时间", "inviteValidityDuration": "请选择持续时间",
"accessRoleSelectPlease": "请选择一个角色", "accessRoleSelectPlease": "请选择一个角色",
"removeOwnAdminRoleConfirmTitle": "移除你的管理员权限?",
"removeOwnAdminRoleConfirmDescription": "保存后,你将不再拥有该组织的管理员权限。如果需要,其他管理员可以恢复访问。",
"removeOwnAdminRoleConfirmButton": "移除我的管理员访问权限",
"removeOwnAdminRoleConfirmPhrase": "移除我的管理员访问",
"ownerMustRetainAdminRole": "组织所有者必须保留至少一个管理员角色。",
"usernameRequired": "必须输入用户名", "usernameRequired": "必须输入用户名",
"idpSelectPlease": "请选择身份提供商", "idpSelectPlease": "请选择身份提供商",
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。", "idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
@@ -630,7 +615,7 @@
"createdAt": "创建于", "createdAt": "创建于",
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。", "proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。", "proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
"proxyEnableSSL": "启用 TLS", "proxyEnableSSL": "启用 SSL",
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保目标的 HTTPS 连接。", "proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保目标的 HTTPS 连接。",
"target": "Target", "target": "Target",
"configureTarget": "配置目标", "configureTarget": "配置目标",
@@ -673,7 +658,6 @@
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。", "targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
"targetsSubmit": "保存目标", "targetsSubmit": "保存目标",
"addTarget": "添加目标", "addTarget": "添加目标",
"proxyMultiSiteRoundRobinNodeHelp": "轮询路由在未连接到相同节点的站点之间将不起作用,但故障转移会生效。",
"targetErrorInvalidIp": "无效的 IP 地址", "targetErrorInvalidIp": "无效的 IP 地址",
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名", "targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
"targetErrorInvalidPort": "无效的端口", "targetErrorInvalidPort": "无效的端口",
@@ -1515,7 +1499,7 @@
"alertingGraphCanvasTitle": "规则流程", "alertingGraphCanvasTitle": "规则流程",
"alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。", "alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。",
"alertingNodeNotConfigured": "尚未配置", "alertingNodeNotConfigured": "尚未配置",
"alertingNodeActionsCount": "{count, plural, other {# 操作}}", "alertingNodeActionsCount": "{count, plural, one {# action} other {# actions}}",
"alertingNodeRoleSource": "来源", "alertingNodeRoleSource": "来源",
"alertingNodeRoleTrigger": "触发", "alertingNodeRoleTrigger": "触发",
"alertingNodeRoleAction": "行为", "alertingNodeRoleAction": "行为",
@@ -2050,7 +2034,7 @@
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "方案", "editInternalResourceDialogScheme": "方案",
"editInternalResourceDialogEnableSsl": "启用 TLS", "editInternalResourceDialogEnableSsl": "启用 SSL",
"editInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。", "editInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。",
"editInternalResourceDialogDestination": "目标", "editInternalResourceDialogDestination": "目标",
"editInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。", "editInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。",
@@ -2067,7 +2051,7 @@
"createInternalResourceDialogName": "名称", "createInternalResourceDialogName": "名称",
"createInternalResourceDialogSite": "站点", "createInternalResourceDialogSite": "站点",
"selectSite": "选择站点...", "selectSite": "选择站点...",
"multiSitesSelectorSitesCount": "{count, plural, other {# 个网站}}", "multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
"noSitesFound": "未找到站点。", "noSitesFound": "未找到站点。",
"createInternalResourceDialogProtocol": "协议", "createInternalResourceDialogProtocol": "协议",
"createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogTcp": "TCP",
@@ -2100,7 +2084,7 @@
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "方案", "scheme": "方案",
"createInternalResourceDialogScheme": "方案", "createInternalResourceDialogScheme": "方案",
"createInternalResourceDialogEnableSsl": "启用 TLS", "createInternalResourceDialogEnableSsl": "启用 SSL",
"createInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。", "createInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。",
"createInternalResourceDialogDestination": "目标", "createInternalResourceDialogDestination": "目标",
"createInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。", "createInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。",
@@ -2233,7 +2217,7 @@
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器", "description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
"introTitle": "托管自托管的潘戈林公司", "introTitle": "托管自托管的潘戈林公司",
"introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。", "introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。",
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 - - 您的隧道、TLS 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:", "introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 - - 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "简单的操作", "title": "简单的操作",
"description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。" "description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。"
@@ -2668,8 +2652,6 @@
"validPassword": "有效密码", "validPassword": "有效密码",
"validEmail": "Valid email", "validEmail": "Valid email",
"validSSO": "Valid SSO", "validSSO": "Valid SSO",
"view": "查看",
"configManaged": "配置已管理",
"connectedClient": "已连接客户端", "connectedClient": "已连接客户端",
"resourceBlocked": "资源被阻止", "resourceBlocked": "资源被阻止",
"droppedByRule": "被规则删除", "droppedByRule": "被规则删除",
@@ -3080,7 +3062,7 @@
"streamingDatadogTitle": "Datadog", "streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。", "streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
"streamingTypePickerDescription": "选择要开始的目标类型。", "streamingTypePickerDescription": "选择要开始的目标类型。",
"streamingLastSyncError": "最后一次同步时发生错误", "streamingFailedToLoad": "加载目的地失败",
"streamingUnexpectedError": "发生意外错误.", "streamingUnexpectedError": "发生意外错误.",
"streamingFailedToUpdate": "更新目标失败", "streamingFailedToUpdate": "更新目标失败",
"streamingDeletedSuccess": "目标删除成功", "streamingDeletedSuccess": "目标删除成功",
@@ -3097,34 +3079,7 @@
"S3DestEditTitle": "编辑目的地", "S3DestEditTitle": "编辑目的地",
"S3DestAddTitle": "添加 S3 目的地", "S3DestAddTitle": "添加 S3 目的地",
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。", "S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
"S3DestAddDescription": "配置一个新的 Amazon S3或兼容 S3 的)存储桶以接收您的组织事件。", "S3DestAddDescription": "配置新的 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 事件流目的地的配置。",

View File

@@ -489,7 +489,7 @@
"createdAt": "創建於", "createdAt": "創建於",
"proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式或將空保存為取消自訂 Header。", "proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式或將空保存為取消自訂 Header。",
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。", "proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
"proxyEnableSSL": "啟用 TLS", "proxyEnableSSL": "啟用 SSL",
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。", "proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
"target": "目標", "target": "目標",
"configureTarget": "配置目標", "configureTarget": "配置目標",
@@ -1763,7 +1763,7 @@
"description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能", "description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能",
"introTitle": "託管式自架 Pangolin", "introTitle": "託管式自架 Pangolin",
"introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。", "introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。",
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 - - 您的隧道、TLS 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:", "introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 - - 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "簡單的操作", "title": "簡單的操作",
"description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。" "description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。"

View File

@@ -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 as typeof db; // is this typeof a problem - techincally they are different types export const primaryDb = db.$primary;
export type Transaction = Parameters< export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0] Parameters<(typeof db)["transaction"]>[0]
>[0]; >[0];

View File

@@ -332,7 +332,6 @@ 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"
}), }),
@@ -440,8 +439,6 @@ 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()
} }

View File

@@ -332,7 +332,6 @@ 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"
}), }),
@@ -446,8 +445,6 @@ 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()
} }

View File

@@ -1227,11 +1227,7 @@ async function getDomainId(
return null; return null;
} }
// Pick the most specific (longest baseDomain) valid domain so that, e.g., const domainSelection = validDomains[0].domains;
// *.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

View File

@@ -25,9 +25,9 @@ import { tierMatrix } from "./billing/tierMatrix";
export async function calculateUserClientsForOrgs( export async function calculateUserClientsForOrgs(
userId: string, userId: string,
trx: Transaction | typeof db = db trx?: Transaction
): Promise<void> { ): Promise<void> {
const execute = async (transaction: Transaction | typeof db) => { const execute = async (transaction: Transaction) => {
const orgCache = new Map<string, typeof orgs.$inferSelect | null>(); const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
const adminRoleCache = new Map< const adminRoleCache = new Map<
string, string,
@@ -437,7 +437,7 @@ export async function calculateUserClientsForOrgs(
async function cleanupOrphanedClients( async function cleanupOrphanedClients(
userId: string, userId: string,
trx: Transaction | typeof db, trx: Transaction,
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

View File

@@ -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.4"; export const APP_VERSION = "1.18.3";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -20,7 +20,9 @@ import {
} from "@server/db"; } from "@server/db";
import { and, eq, inArray, ne } from "drizzle-orm"; import { and, eq, inArray, ne } from "drizzle-orm";
import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers"; import {
deletePeer as newtDeletePeer
} from "@server/routers/newt/peers";
import { import {
initPeerAddHandshake, initPeerAddHandshake,
deletePeer as olmDeletePeer deletePeer as olmDeletePeer
@@ -31,7 +33,7 @@ import {
generateAliasConfig, generateAliasConfig,
generateRemoteSubnets, generateRemoteSubnets,
generateSubnetProxyTargetV2, generateSubnetProxyTargetV2,
parseEndpoint parseEndpoint,
} from "@server/lib/ip"; } from "@server/lib/ip";
import { import {
addPeerData, addPeerData,
@@ -49,7 +51,10 @@ export async function getClientSiteResourceAccess(
? await trx ? await trx
.select() .select()
.from(sites) .from(sites)
.innerJoin(siteNetworks, eq(siteNetworks.siteId, sites.siteId)) .innerJoin(
siteNetworks,
eq(siteNetworks.siteId, sites.siteId)
)
.where(eq(siteNetworks.networkId, siteResource.networkId)) .where(eq(siteNetworks.networkId, siteResource.networkId))
.then((rows) => rows.map((row) => row.sites)) .then((rows) => rows.map((row) => row.sites))
: []; : [];
@@ -357,8 +362,7 @@ export async function rebuildClientAssociationsFromSiteResource(
.where(inArray(clients.clientId, existingClientSiteIds)) .where(inArray(clients.clientId, existingClientSiteIds))
: []; : [];
const otherResourceClientIds = const otherResourceClientIds = clientsFromOtherResourcesBySite.get(siteId) ?? new Set<number>();
clientsFromOtherResourcesBySite.get(siteId) ?? new Set<number>();
logger.debug( logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]` `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
@@ -705,7 +709,7 @@ export async function updateClientSiteDestinations(
sourcePort: destination.sourcePort, sourcePort: destination.sourcePort,
destinations: destination.destinations destinations: destination.destinations
}; };
logger.debug( logger.info(
`Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}`
); );

View File

@@ -124,7 +124,7 @@ export function computeBuckets(
let totalDowntime = 0; let totalDowntime = 0;
for (let d = 0; d < days; d++) { for (let d = 0; d < days; d++) {
const dayStartSec = todayMidnightSec - (days - 1 - d) * 86400; const dayStartSec = todayMidnightSec - (days - d) * 86400;
const dayEndSec = dayStartSec + 86400; const dayEndSec = dayStartSec + 86400;
const dayEvents = events.filter( const dayEvents = events.filter(

View File

@@ -485,133 +485,6 @@ 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[];
@@ -702,16 +575,18 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
} }
for (const cert of allCerts) { for (const cert of allCerts) {
const mainDomain = cert?.domain?.main; const domain = cert?.domain?.main;
if (!mainDomain || typeof mainDomain !== "string") { if (!domain || typeof domain !== "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 ${mainDomain} - empty certificate or key field` `acmeCertSync: skipping cert for ${domain} - empty certificate or key field`
); );
continue; continue;
} }
@@ -723,14 +598,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 ${mainDomain} - failed to base64-decode cert/key: ${err}` `acmeCertSync: skipping cert for ${domain} - 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 ${mainDomain} - blank PEM after base64 decode` `acmeCertSync: skipping cert for ${domain} - blank PEM after base64 decode`
); );
continue; continue;
} }
@@ -741,7 +616,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 ${mainDomain} - no PEM certificate block found` `acmeCertSync: skipping cert for ${domain} - no PEM certificate block found`
); );
continue; continue;
} }
@@ -753,7 +628,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
); );
} catch (err) { } catch (err) {
logger.debug( logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - invalid X.509 certificate: ${err}` `acmeCertSync: skipping cert for ${domain} - invalid X.509 certificate: ${err}`
); );
continue; continue;
} }
@@ -763,40 +638,139 @@ 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 ${mainDomain} - invalid private key: ${err}` `acmeCertSync: skipping cert for ${domain} - invalid private key: ${err}`
); );
continue; continue;
} }
// Collect all domains covered by this cert: main + every SAN. // Check if cert already exists in DB
// Each domain gets its own row in the certificates table so that const existing = await db
// lookups by any hostname on the cert succeed independently. .select()
const allDomains = new Set<string>([mainDomain]); .from(certificates)
if (Array.isArray(cert.domain?.sans)) { .where(and(eq(certificates.domain, domain)))
for (const san of cert.domain.sans) { .limit(1);
if (typeof san === "string" && san.trim()) {
allDomains.add(san.trim());
}
}
}
logger.debug( let oldCertPem: string | null = null;
`acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}` let oldKeyPem: string | null = null;
);
for (const domain of allDomains) { if (existing.length > 0 && existing[0].certFile) {
try { try {
await storeCertForDomain( const storedCertPem = decrypt(
domain, existing[0].certFile,
certPem, config.getRawConfig().server.secret!
keyPem, );
validatedX509 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
let expiresAt: number | null = null;
try {
expiresAt = Math.floor(
new Date(validatedX509.validTo).getTime() / 1000
); );
} catch (err) { } catch (err) {
logger.error( logger.debug(
`acmeCertSync: error storing cert for domain "${domain}": ${err}` `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"})`
);
// For a brand-new cert, push to any SSL resources that were waiting for it
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
} }
} }
} }

View File

@@ -97,13 +97,6 @@ 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 || "";

View File

@@ -46,7 +46,6 @@ 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;

View File

@@ -30,12 +30,10 @@ 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";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -206,10 +204,7 @@ export class LogStreamingManager {
this.pollTimer = null; this.pollTimer = null;
this.runPoll() this.runPoll()
.catch((err) => .catch((err) =>
logger.error( logger.error("LogStreamingManager: unexpected poll error", err)
"LogStreamingManager: unexpected poll error",
err
)
) )
.finally(() => { .finally(() => {
if (this.isRunning) { if (this.isRunning) {
@@ -280,13 +275,10 @@ 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: unknown; let configFromDb: HttpConfig;
try { try {
const decryptedConfig = decrypt( const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
dest.config, configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
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`,
@@ -313,7 +305,6 @@ 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;
@@ -321,10 +312,6 @@ 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}`,
@@ -335,10 +322,6 @@ 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)) {
@@ -347,7 +330,6 @@ export class LogStreamingManager {
`LogStreamingManager: destination ${dest.destinationId} recovered` `LogStreamingManager: destination ${dest.destinationId} recovered`
); );
} }
await this.clearDestinationError(dest.destinationId);
} }
} }
@@ -380,10 +362,7 @@ export class LogStreamingManager {
.from(eventStreamingCursors) .from(eventStreamingCursors)
.where( .where(
and( and(
eq( eq(eventStreamingCursors.destinationId, dest.destinationId),
eventStreamingCursors.destinationId,
dest.destinationId
),
eq(eventStreamingCursors.logType, logType) eq(eventStreamingCursors.logType, logType)
) )
) )
@@ -452,7 +431,9 @@ export class LogStreamingManager {
if (rows.length === 0) break; if (rows.length === 0) break;
const events = rows.map((row) => this.rowToLogEvent(logType, row)); const events = rows.map((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);
@@ -696,7 +677,8 @@ export class LogStreamingManager {
break; break;
} }
const orgId = typeof row.orgId === "string" ? row.orgId : ""; const orgId =
typeof row.orgId === "string" ? row.orgId : "";
return { return {
id: row.id, id: row.id,
@@ -726,8 +708,6 @@ 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:
@@ -769,45 +749,6 @@ 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[]
> { > {

View File

@@ -1,279 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { 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());
}
}

View File

@@ -107,40 +107,6 @@ 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)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -141,7 +141,6 @@ 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()

View File

@@ -124,11 +124,15 @@ function getWhere(data: Q) {
data.clientId data.clientId
? eq(connectionAuditLog.clientId, data.clientId) ? eq(connectionAuditLog.clientId, data.clientId)
: undefined, : undefined,
data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined, data.siteId
? eq(connectionAuditLog.siteId, data.siteId)
: undefined,
data.siteResourceId data.siteResourceId
? eq(connectionAuditLog.siteResourceId, data.siteResourceId) ? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
: undefined, : undefined,
data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined data.userId
? eq(connectionAuditLog.userId, data.userId)
: undefined
); );
} }
@@ -140,7 +144,6 @@ 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,
@@ -200,7 +203,10 @@ async function enrichWithDetails(
]; ];
// Fetch resource details from main database // Fetch resource details from main database
const resourceMap = new Map<number, { name: string; niceId: string }>(); const resourceMap = new Map<
number,
{ name: string; niceId: string }
>();
if (siteResourceIds.length > 0) { if (siteResourceIds.length > 0) {
const resourceDetails = await primaryDb const resourceDetails = await primaryDb
.select({ .select({
@@ -262,7 +268,10 @@ async function enrichWithDetails(
} }
// Fetch user details from main database // Fetch user details from main database
const userMap = new Map<string, { email: string | null }>(); const userMap = new Map<
string,
{ email: string | null }
>();
if (userIds.length > 0) { if (userIds.length > 0) {
const userDetails = await primaryDb const userDetails = await primaryDb
.select({ .select({
@@ -281,25 +290,29 @@ 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 ? (userMap.get(log.userId)?.email ?? null) : null userEmail: log.userId
? userMap.get(log.userId)?.email ?? null
: null
})); }));
} }

View File

@@ -51,8 +51,6 @@ 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;
@@ -81,8 +79,7 @@ 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: description: "List all event streaming destinations for a specific organization.",
"List all event streaming destinations for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.Org],
request: { request: {
query: querySchema, query: querySchema,

View File

@@ -11,7 +11,7 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { clientSitesAssociationsCache, db } from "@server/db"; import { 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,11 +146,7 @@ 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) {
@@ -176,21 +172,9 @@ 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),
@@ -205,8 +189,7 @@ 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
}); });
} }
} }
@@ -251,7 +234,6 @@ 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,

View File

@@ -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, primaryDb, Client } from "@server/db"; import { clients, db } 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,6 +98,15 @@ 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)
@@ -113,12 +122,8 @@ export async function addUserRole(
); );
} }
let newUserRole: { let newUserRole: { userId: string; orgId: string; roleId: number } | null =
userId: string; null;
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)
@@ -144,19 +149,11 @@ export async function addUserRole(
) )
); );
orgClientsToRebuild = orgClients; for (const orgClient of 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,

View File

@@ -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, primaryDb, Client } from "@server/db"; import { db } 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 && role.isAdmin === true) { if (existingUser.isOwner) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"Cannot remove the administrator role from the organization owner" "Cannot change the roles of the owner of the organization"
) )
); );
} }
@@ -129,7 +129,6 @@ 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)
@@ -151,19 +150,11 @@ export async function removeUserRole(
) )
); );
orgClientsToRebuild = orgClients; for (const orgClient of 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,

View File

@@ -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, primaryDb, Client } from "@server/db"; import { clients, db } 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,8 +87,17 @@ 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, isAdmin: roles.isAdmin }) .select({ roleId: roles.roleId })
.from(roles) .from(roles)
.where( .where(
and( and(
@@ -106,19 +115,6 @@ 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)
@@ -146,19 +142,11 @@ export async function setUserOrgRoles(
and(eq(clients.userId, userId), eq(clients.orgId, orgId)) and(eq(clients.userId, userId), eq(clients.orgId, orgId))
); );
orgClientsToRebuild = orgClients; for (const orgClient of 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,

View File

@@ -100,7 +100,6 @@ 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;

View File

@@ -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, primaryDb } from "@server/db"; import { db, orgs, userOrgs, users } 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,18 +218,13 @@ 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) {

View File

@@ -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, primaryDb } from "@server/db"; import { db } from "@server/db";
import { import {
roles, roles,
Client, Client,
@@ -92,10 +92,7 @@ export async function createClient(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if ( if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
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")
); );
@@ -201,10 +198,7 @@ export async function createClient(
if (!randomExitNode) { if (!randomExitNode) {
return next( return next(
createHttpError( createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
HttpCode.NOT_FOUND,
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
)
); );
} }
@@ -262,17 +256,9 @@ export async function createClient(
clientId: newClient.clientId, clientId: newClient.clientId,
dateCreated: moment().toISOString() dateCreated: moment().toISOString()
}); });
});
if (newClient) { await rebuildClientAssociationsFromClient(newClient, trx);
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,

View File

@@ -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, primaryDb } from "@server/db"; import { db } from "@server/db";
import { import {
roles, roles,
Client, Client,
@@ -237,17 +237,9 @@ export async function createUserClient(
userId, userId,
clientId: newClient.clientId clientId: newClient.clientId
}); });
});
if (newClient) { await rebuildClientAssociationsFromClient(newClient, trx);
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,

View File

@@ -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, primaryDb, Client, Olm } from "@server/db"; import { db, olms } 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,17 +71,14 @@ 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
[deletedClient] = await trx const [deletedClient] = await trx
.delete(clients) .delete(clients)
.where(eq(clients.clientId, clientId)) .where(eq(clients.clientId, clientId))
.returning(); .returning();
[olm] = await trx const [olm] = await trx
.select() .select()
.from(olms) .from(olms)
.where(eq(olms.clientId, clientId)) .where(eq(olms.clientId, clientId))
@@ -91,28 +88,13 @@ 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));
} }
});
if (deletedClient) { await rebuildClientAssociationsFromClient(deletedClient, trx);
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
(e) => {
logger.error(
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
);
}
);
if (olm) { if (olm) {
sendTerminateClient( await sendTerminateClient(deletedClient.clientId, OlmErrorCodes.TERMINATED_DELETED, olm.olmId); // the olmId needs to be provided because it cant look it up after deletion
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,

View File

@@ -27,11 +27,11 @@ export async function buildSiteConfigurationForOlmClient(
) { ) {
const siteConfigurations: { const siteConfigurations: {
siteId: number; siteId: number;
name?: string; name?: string
endpoint?: string; endpoint?: string
publicKey?: string; publicKey?: string
serverIP?: string | null; serverIP?: string | null
serverPort?: number | null; serverPort?: number | null
remoteSubnets?: string[]; remoteSubnets?: string[];
aliases: Alias[]; aliases: Alias[];
}[] = []; }[] = [];
@@ -79,6 +79,7 @@ export async function buildSiteConfigurationForOlmClient(
) )
); );
if (jitMode) { if (jitMode) {
// Add site configuration to the array // Add site configuration to the array
siteConfigurations.push({ siteConfigurations.push({
@@ -108,9 +109,10 @@ export async function buildSiteConfigurationForOlmClient(
continue; continue;
} }
if (!site.publicKey || site.publicKey == "") { if (!site.publicKey || site.publicKey == "") { // the site is not ready to accept new peers
// the site is not ready to accept new peers logger.warn(
logger.warn(`Site ${site.siteId} has no public key, skipping`); `Site ${site.siteId} has no public key, skipping`
);
continue; continue;
} }

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db, olms, primaryDb } from "@server/db"; import { db, olms } 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,7 +81,8 @@ export async function createUserOlm(
const secretHash = await hashPassword(secret); const secretHash = await hashPassword(secret);
await db.insert(olms).values({ await db.transaction(async (trx) => {
await trx.insert(olms).values({
olmId: olmId, olmId: olmId,
userId, userId,
name, name,
@@ -89,11 +90,7 @@ export async function createUserOlm(
dateCreated: moment().toISOString() dateCreated: moment().toISOString()
}); });
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { await calculateUserClientsForOrgs(userId, trx);
console.error(
"Error calculating user clients after creating olm:",
e
);
}); });
return response<CreateOlmResponse>(res, { return response<CreateOlmResponse>(res, {

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { Client, db, Olm, primaryDb } from "@server/db"; import { Client, db } 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,7 +49,6 @@ 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
@@ -58,6 +57,7 @@ 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,27 +67,22 @@ export async function deleteUserOlm(
} }
// Finally, delete the OLM itself // Finally, delete the OLM itself
await trx.delete(olms).where(eq(olms.olmId, olmId)).returning(); const [olm] = await trx
}); .delete(olms)
.where(eq(olms.olmId, olmId))
.returning();
if (deletedClient) { if (deletedClient) {
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch( await rebuildClientAssociationsFromClient(deletedClient, trx);
(e) => { if (olm) {
logger.error( await sendTerminateClient(
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
);
}
);
sendTerminateClient(
deletedClient.clientId, deletedClient.clientId,
OlmErrorCodes.TERMINATED_DELETED, OlmErrorCodes.TERMINATED_DELETED,
olmId olm.olmId
).catch((e) => { ); // the olmId needs to be provided because it cant look it up after deletion
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,

View File

@@ -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("[handleOlmRegisterMessage] Handling register olm message"); logger.info("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("[handleOlmRegisterMessage] Olm not found"); logger.warn("Olm not found");
return; return;
} }
@@ -46,19 +46,16 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
} = message.data; } = message.data;
if (!olm.clientId) { if (!olm.clientId) {
logger.warn("[handleOlmRegisterMessage] Olm client ID not found"); logger.warn("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( logger.debug("Handling fingerprint insertion for olm register...", {
"[handleOlmRegisterMessage] Handling fingerprint insertion for olm register...",
{
olmId: olm.olmId, olmId: olm.olmId,
fingerprint, fingerprint,
postures postures
} });
);
const isUserDevice = olm.userId !== null && olm.userId !== undefined; const isUserDevice = olm.userId !== null && olm.userId !== undefined;
@@ -88,17 +85,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.limit(1); .limit(1);
if (!client) { if (!client) {
logger.warn("[handleOlmRegisterMessage] Client not found", { logger.warn("Client ID 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(
`[handleOlmRegisterMessage] Client ${client.clientId} is blocked. Ignoring register.`, `Client ${client.clientId} is blocked. Ignoring register.`
{ orgId: client.orgId }
); );
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId); sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
return; return;
@@ -106,8 +100,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
if (client.approvalState == "pending") { if (client.approvalState == "pending") {
logger.debug( logger.debug(
`[handleOlmRegisterMessage] Client ${client.clientId} approval is pending. Ignoring register.`, `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;
@@ -135,18 +128,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.limit(1); .limit(1);
if (!org) { if (!org) {
logger.warn("[handleOlmRegisterMessage] Org not found", { logger.warn("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("[handleOlmRegisterMessage] Olm has no user ID", { logger.warn("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;
} }
@@ -154,18 +143,12 @@ 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( logger.warn("Invalid user session for olm register");
"[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( logger.warn("User ID mismatch for olm register");
"[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;
} }
@@ -180,15 +163,11 @@ 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("[handleOlmRegisterMessage] Policy check result", { logger.debug("Policy check result:", policyCheck);
orgId: client.orgId,
policyCheck
});
if (policyCheck?.error) { if (policyCheck?.error) {
logger.error( logger.error(
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`, `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;
@@ -196,8 +175,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
if (policyCheck.policies?.passwordAge?.compliant === false) { if (policyCheck.policies?.passwordAge?.compliant === false) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`, `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,
@@ -208,8 +186,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
policyCheck.policies?.maxSessionLength?.compliant === false policyCheck.policies?.maxSessionLength?.compliant === false
) { ) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant session length for org ${orgId}`, `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,
@@ -218,8 +195,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} else if (policyCheck.policies?.requiredTwoFactor === false) { } else if (policyCheck.policies?.requiredTwoFactor === false) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`, `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,
@@ -228,8 +204,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} else if (!policyCheck.allowed) { } else if (!policyCheck.allowed) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`, `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;
@@ -251,39 +226,29 @@ 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( logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
`[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( logger.info("Too many sites (%d), dropping into JIT mode", sitesCount);
`[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`,
{ orgId: client.orgId }
);
jitMode = true; jitMode = true;
} }
logger.debug( logger.debug(
`[handleOlmRegisterMessage] Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`, `Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
{ orgId: client.orgId }
); );
if (!publicKey) { if (!publicKey) {
logger.warn("[handleOlmRegisterMessage] Public key not provided", { logger.warn("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(
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...", "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
@@ -309,8 +274,7 @@ 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(
`[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}?`, `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;
} }

View File

@@ -17,7 +17,7 @@ import { initPeerAddHandshake } from "./peers";
export const handleOlmServerInitAddPeerHandshake: MessageHandler = async ( export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
context context
) => { ) => {
logger.info("Handle Olm Server Init Add Peer Handshake Message"); logger.info("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;

View File

@@ -9,50 +9,16 @@ import {
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
import { sendToClient } from "#dynamic/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger"; import logger from "@server/logger";
import { count, eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { canCompress } from "@server/lib/clientVersionChecks"; import { canCompress } from "@server/lib/clientVersionChecks";
import { build } from "@server/build";
export async function sendOlmSyncMessage(olm: Olm, client: Client) { export async function sendOlmSyncMessage(olm: Olm, client: Client) {
// Get all sites data
const sitesCountResult = await db
.select({ count: count() })
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract the count value from the result array
const sitesCount =
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
// Prepare an array to store site configurations
logger.debug(
`[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`,
{ orgId: client.orgId }
);
let jitMode = false;
if (sitesCount > 250 && build == "saas") {
// THIS IS THE MAX ON THE BUSINESS TIER
// 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
logger.info(
`[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`,
{ orgId: client.orgId }
);
jitMode = true;
}
// NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT // NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT
const siteConfigurations = await buildSiteConfigurationForOlmClient( const siteConfigurations = await buildSiteConfigurationForOlmClient(
client, client,
client.pubKey, client.pubKey,
false, false
jitMode
); );
// Get all exit nodes from sites where the client has peers // Get all exit nodes from sites where the client has peers
@@ -116,6 +82,7 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) {
exitNodes: exitNodesData exitNodes: exitNodesData
} }
}, },
{ {
compress: canCompress(olm.version, "olm") compress: canCompress(olm.version, "olm")
} }

View File

@@ -5,8 +5,7 @@ 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";
@@ -221,12 +220,8 @@ export async function batchAddClientToSiteResources(
siteResourceId: siteResource.siteResourceId siteResourceId: siteResource.siteResourceId
}); });
} }
});
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => { await rebuildClientAssociationsFromClient(client, trx);
logger.error(
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
);
}); });
return response(res, { return response(res, {

View File

@@ -10,8 +10,7 @@ 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 {
@@ -520,10 +519,12 @@ 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.
rebuildClientAssociationsFromSiteResource( db.transaction(async (trx) => {
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

View File

@@ -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, primaryDb, sites } from "@server/db"; import { db, newts, 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,10 +73,12 @@ 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.
rebuildClientAssociationsFromSiteResource( db.transaction(async (trx) => {
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

View File

@@ -1,13 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, orgs, primaryDb } from "@server/db"; import { db, orgs } from "@server/db";
import { import { roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
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";
@@ -152,7 +146,9 @@ export async function acceptInvite(
.from(userInviteRoles) .from(userInviteRoles)
.where(eq(userInviteRoles.inviteId, inviteId)); .where(eq(userInviteRoles.inviteId, inviteId));
const inviteRoleIds = [...new Set(inviteRoleRows.map((r) => r.roleId))]; const inviteRoleIds = [
...new Set(inviteRoleRows.map((r) => r.roleId))
];
if (inviteRoleIds.length === 0) { if (inviteRoleIds.length === 0) {
return next( return next(
createHttpError( createHttpError(
@@ -197,19 +193,13 @@ 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,

View File

@@ -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, primaryDb, Client } from "@server/db"; import { clients, db } 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 && role.isAdmin !== true) { if (existingUser.isOwner) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"The organization owner must retain an administrator role" "Cannot change the role of the owner of the organization"
) )
); );
} }
@@ -112,8 +112,6 @@ 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)
@@ -140,19 +138,11 @@ export async function addUserRoleLegacy(
) )
); );
orgClientsToRebuild = orgClients; for (const orgClient of 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,

View File

@@ -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, primaryDb } from "@server/db"; import { db } 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,12 +53,8 @@ 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));
});
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { await calculateUserClientsForOrgs(userId, trx);
logger.error(
`Failed to calculate user clients after removing user ${userId}: ${e}`
);
}); });
return response(res, { return response(res, {

View File

@@ -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, primaryDb } from "@server/db"; import { db, orgs } 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,7 +34,8 @@ const bodySchema = z
roleId: z.number().int().positive().optional() roleId: z.number().int().positive().optional()
}) })
.refine( .refine(
(d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null, (d) =>
(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) => ({
@@ -99,14 +100,8 @@ export async function createOrgUser(
} }
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const { const { username, email, name, type, idpId, roleIds: uniqueRoleIds } =
username, parsedBody.data;
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);
@@ -237,7 +232,6 @@ 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()
@@ -276,7 +270,7 @@ export async function createOrgUser(
{ {
orgId, orgId,
userId: existingUser.userId, userId: existingUser.userId,
autoProvisioned: false autoProvisioned: false,
}, },
uniqueRoleIds, uniqueRoleIds,
trx trx
@@ -303,25 +297,15 @@ export async function createOrgUser(
{ {
orgId, orgId,
userId: newUser.userId, userId: newUser.userId,
autoProvisioned: false autoProvisioned: false,
}, },
uniqueRoleIds, uniqueRoleIds,
trx trx
); );
} }
userIdForClients = userId; await calculateUserClientsForOrgs(userId, trx);
}); });
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")

View File

@@ -47,7 +47,10 @@ export async function queryUser(orgId: string, userId: string) {
.from(userOrgRoles) .from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId)) .leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where( .where(
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId)) and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
); );
const isAdmin = roleRows.some((r) => r.isAdmin); const isAdmin = roleRows.some((r) => r.isAdmin);
@@ -58,8 +61,7 @@ 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
})) }))
}; };
} }

View File

@@ -7,8 +7,7 @@ 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";
@@ -92,12 +91,25 @@ export async function removeUserOrg(
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await removeUserFromOrg(org, userId, trx); await removeUserFromOrg(org, userId, trx);
});
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { // if (build === "saas") {
logger.error( // const [rootUser] = await trx
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}` // .select()
); // .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, {

View File

@@ -24,7 +24,6 @@ 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
@@ -48,8 +47,7 @@ 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;

View File

@@ -42,7 +42,6 @@ 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
@@ -82,8 +81,7 @@ 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;

View File

@@ -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 IF NOT EXISTS "trialNotifications" ( CREATE TABLE "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,6 +52,10 @@ 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) {

View File

@@ -1,34 +0,0 @@
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`);
}

View File

@@ -16,7 +16,7 @@ export default async function migration() {
db.transaction(() => { db.transaction(() => {
db.prepare( db.prepare(
` `
CREATE TABLE IF NOT EXISTS 'trialNotifications' ( CREATE TABLE '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,

View File

@@ -1,43 +0,0 @@
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`);
}

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import IdpTypeBadge from "@app/components/IdpTypeBadge"; import IdpTypeBadge from "@app/components/IdpTypeBadge";
import OrgRolesTagField from "@app/components/OrgRolesTagField"; import OrgRolesTagField from "@app/components/OrgRolesTagField";
import { import {
@@ -26,7 +25,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; 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 { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -34,7 +32,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useActionState, useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -44,15 +42,13 @@ 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 });
@@ -76,8 +72,7 @@ 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
})) }))
} }
}); });
@@ -89,8 +84,7 @@ 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
})) }))
); );
form.setValue("autoProvisioned", user.autoProvisioned || false); form.setValue("autoProvisioned", user.autoProvisioned || false);
@@ -101,11 +95,11 @@ export default function AccessControlsPage() {
? t("singleRolePerUserPlanNotice") ? t("singleRolePerUserPlanNotice")
: t("singleRolePerUserEditionNotice"); : t("singleRolePerUserEditionNotice");
const [isSaving, setIsSaving] = useState(false); const [, action, isSubmitting] = useActionState(onSubmit, null);
const [confirmRemoveOwnAdminOpen, setConfirmRemoveOwnAdminOpen] = async function onSubmit() {
useState(false); const isValid = await form.trigger();
if (!isValid) return;
async function executeSave() {
const values = form.getValues(); const values = form.getValues();
if (values.roles.length === 0) { if (values.roles.length === 0) {
@@ -117,7 +111,6 @@ export default function AccessControlsPage() {
return; return;
} }
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
@@ -137,8 +130,7 @@ 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
}); });
@@ -157,61 +149,11 @@ export default function AccessControlsPage() {
t("accessRoleErrorAddDescription") t("accessRoleErrorAddDescription")
) )
}); });
} finally {
setIsSaving(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>
@@ -226,7 +168,7 @@ export default function AccessControlsPage() {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={(e) => void handleAccessControlsSubmit(e)} action={action}
className="space-y-4" className="space-y-4"
id="access-controls-form" id="access-controls-form"
> >
@@ -295,8 +237,8 @@ export default function AccessControlsPage() {
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button
type="submit" type="submit"
loading={isSaving} loading={isSubmitting}
disabled={isSaving} disabled={isSubmitting}
form="access-controls-form" form="access-controls-form"
> >
{t("accessControlsSubmit")} {t("accessControlsSubmit")}

View File

@@ -645,12 +645,6 @@ 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 && (

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useEffect, useCallback } from "react"; import { 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,18 +22,7 @@ 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 { import { Globe, MoreHorizontal, Plus } from "lucide-react";
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";
@@ -49,10 +38,7 @@ import {
HttpDestinationCredenza, HttpDestinationCredenza,
parseHttpConfig parseHttpConfig
} from "@app/components/HttpDestinationCredenza"; } from "@app/components/HttpDestinationCredenza";
import { import { S3DestinationCredenza } from "@app/components/S3DestinationCredenza";
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";
@@ -78,42 +64,6 @@ 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,
@@ -123,25 +73,25 @@ function DestinationCard({
disabled = false disabled = false
}: DestinationCardProps) { }: DestinationCardProps) {
const t = useTranslations(); const t = useTranslations();
const { name, typeLabel, detail, icon } = const cfg = parseHttpConfig(destination.config);
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">
{icon} <Globe className="h-3.5 w-3.5 text-black" />
</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">
{name || t("streamingUnnamedDestination")} {cfg.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">
{typeLabel} HTTP
</p> </p>
</div> </div>
</div> </div>
@@ -155,40 +105,15 @@ function DestinationCard({
/> />
</div> </div>
{/* Detail preview (URL for HTTP, s3:// path for S3) */} {/* URL preview */}
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground truncate">
{detail || ( {cfg.url || (
<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
@@ -560,7 +485,7 @@ export default function StreamingDestinationsPage() {
if (!v) setDeleteTarget(null); if (!v) setDeleteTarget(null);
}} }}
string={ string={
getDestinationDisplay(deleteTarget).name || parseHttpConfig(deleteTarget.config).name ||
t("streamingDeleteDialogThisDestination") t("streamingDeleteDialogThisDestination")
} }
title={t("streamingDeleteTitle")} title={t("streamingDeleteTitle")}
@@ -568,7 +493,7 @@ export default function StreamingDestinationsPage() {
<p> <p>
{t("streamingDeleteDialogAreYouSure")}{" "} {t("streamingDeleteDialogAreYouSure")}{" "}
<span> <span>
{getDestinationDisplay(deleteTarget).name || {parseHttpConfig(deleteTarget.config).name ||
t("streamingDeleteDialogThisDestination")} t("streamingDeleteDialogThisDestination")}
</span> </span>
{t("streamingDeleteDialogPermanentlyRemoved")} {t("streamingDeleteDialogPermanentlyRemoved")}

View File

@@ -84,7 +84,6 @@ import {
AlertTriangle, AlertTriangle,
CircleCheck, CircleCheck,
CircleX, CircleX,
ExternalLink,
Info, Info,
Plus, Plus,
Settings Settings
@@ -962,18 +961,13 @@ 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"> <p className="text-sm text-muted-foreground mt-3 flex items-start gap-1.5">
{t("proxyMultiSiteRoundRobinNodeHelp")}{" "} <AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
<a <span>
href="https://docs.pangolin.net/manage/resources/public/targets#distributing-sites-load-across-servers" Round robin routing will not work between
target="_blank" sites that are not connected to the same
rel="noopener noreferrer" node, but failover will work.
className="text-primary hover:underline inline-flex items-center gap-1" </span>
>
{t("learnMore")}
<ExternalLink className="size-3.5 shrink-0" />
</a>
.
</p> </p>
)} )}
</SettingsSectionBody> </SettingsSectionBody>

View File

@@ -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,22 +1425,16 @@ export default function Page() {
</Button> </Button>
</div> </div>
)} )}
{build === "saas" && {build === "enterprise" &&
targets.length > 1 && targets.length > 1 &&
new Set(targets.map((t) => t.siteId)).size > new Set(targets.map((t) => t.siteId)).size > 1 && (
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"> <InfoIcon 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>

View File

@@ -55,9 +55,7 @@ export default async function ProxyResourcesPage(
pagination = responseData.pagination; pagination = responseData.pagination;
} catch (e) {} } catch (e) {}
const siteIdParam = parsePositiveInt( const siteIdParam = parsePositiveInt(searchParams.get("siteId") ?? undefined);
searchParams.get("siteId") ?? undefined
);
let initialFilterSite: { let initialFilterSite: {
siteId: number; siteId: number;
@@ -124,7 +122,6 @@ 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,

View File

@@ -13,8 +13,6 @@ 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;
@@ -51,22 +49,7 @@ export default function DomainPageClient({
<> <>
<div className="flex justify-between"> <div className="flex justify-between">
<SettingsSectionTitle <SettingsSectionTitle
title={ title={domain.baseDomain}
<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 ? (

View File

@@ -16,7 +16,6 @@ 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";
@@ -73,11 +72,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
const { org } = useOrgContext(); const { org } = useOrgContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const { data: rawDomains, isRefetching, refetch } = useQuery({
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")
@@ -85,15 +80,12 @@ export default function DomainsTable({ domains, orgId }: Props) {
const tableData = useMemo( const tableData = useMemo(
() => () =>
(rawDomains ?? []).map( (rawDomains ?? []).map((d) => ({
(d) =>
({
...d, ...d,
baseDomain: toUnicode(d.baseDomain), baseDomain: toUnicode(d.baseDomain),
type: d.type ?? "", type: d.type ?? "",
errorMessage: d.errorMessage ?? null errorMessage: d.errorMessage ?? null
}) as DomainRow } as DomainRow)),
),
[rawDomains] [rawDomains]
); );
@@ -206,17 +198,12 @@ export default function DomainsTable({ domains, orgId }: Props) {
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Badge <Badge variant="red" className="cursor-help">
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"> <p className="break-words">{errorMessage}</p>
{errorMessage}
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -233,17 +220,12 @@ export default function DomainsTable({ domains, orgId }: Props) {
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Badge <Badge variant="yellow" className="cursor-help">
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"> <p className="break-words">{errorMessage}</p>
{errorMessage}
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -271,25 +253,6 @@ 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] : []),
@@ -320,7 +283,6 @@ export default function DomainsTable({ domains, orgId }: Props) {
{t("viewSettings")} {t("viewSettings")}
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
{!domain.configManaged && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setSelectedDomain(domain); setSelectedDomain(domain);
@@ -331,7 +293,6 @@ export default function DomainsTable({ domains, orgId }: Props) {
{t("delete")} {t("delete")}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{domain.failed && ( {domain.failed && (
@@ -354,9 +315,7 @@ 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"}>
{domain.configManaged {t("edit")}
? 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>

View File

@@ -19,8 +19,7 @@ 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, AlertCircle } from "lucide-react"; import { Plus, X } 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";
@@ -57,8 +56,6 @@ 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;
} }
@@ -125,7 +122,9 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
/> />
<Input <Input
value={h.value} value={h.value}
onChange={(e) => updateRow(i, "value", e.target.value)} onChange={(e) =>
updateRow(i, "value", e.target.value)
}
placeholder={t("httpDestHeaderValuePlaceholder")} placeholder={t("httpDestHeaderValuePlaceholder")}
className="flex-1" className="flex-1"
/> />
@@ -201,7 +200,10 @@ export function HttpDestinationCredenza({
if (!raw) return null; if (!raw) return null;
try { try {
const parsed = new URL(raw); const parsed = new URL(raw);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { if (
parsed.protocol !== "http:" &&
parsed.protocol !== "https:"
) {
return t("httpDestUrlErrorHttpRequired"); return t("httpDestUrlErrorHttpRequired");
} }
if (build === "saas" && parsed.protocol !== "https:") { if (build === "saas" && parsed.protocol !== "https:") {
@@ -214,7 +216,9 @@ export function HttpDestinationCredenza({
})(); })();
const isValid = const isValid =
cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null; cfg.name.trim() !== "" &&
cfg.url.trim() !== "" &&
urlError === null;
async function handleSave() { async function handleSave() {
if (!isValid) return; if (!isValid) return;
@@ -249,7 +253,10 @@ export function HttpDestinationCredenza({
title: editing title: editing
? t("httpDestUpdateFailed") ? t("httpDestUpdateFailed")
: t("httpDestCreateFailed"), : t("httpDestCreateFailed"),
description: formatAxiosError(e, t("streamingUnexpectedError")) description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
}); });
} finally { } finally {
setSaving(false); setSaving(false);
@@ -273,14 +280,6 @@ 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={[
@@ -358,9 +357,7 @@ 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( {t("httpDestAuthNoneDescription")}
"httpDestAuthNoneDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -378,21 +375,15 @@ export function HttpDestinationCredenza({
htmlFor="auth-bearer" htmlFor="auth-bearer"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
{t( {t("httpDestAuthBearerTitle")}
"httpDestAuthBearerTitle"
)}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{t( {t("httpDestAuthBearerDescription")}
"httpDestAuthBearerDescription"
)}
</p> </p>
</div> </div>
{cfg.authType === "bearer" && ( {cfg.authType === "bearer" && (
<Input <Input
placeholder={t( placeholder={t("httpDestAuthBearerPlaceholder")}
"httpDestAuthBearerPlaceholder"
)}
value={ value={
cfg.bearerToken ?? "" cfg.bearerToken ?? ""
} }
@@ -420,21 +411,15 @@ export function HttpDestinationCredenza({
htmlFor="auth-basic" htmlFor="auth-basic"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
{t( {t("httpDestAuthBasicTitle")}
"httpDestAuthBasicTitle"
)}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{t( {t("httpDestAuthBasicDescription")}
"httpDestAuthBasicDescription"
)}
</p> </p>
</div> </div>
{cfg.authType === "basic" && ( {cfg.authType === "basic" && (
<Input <Input
placeholder={t( placeholder={t("httpDestAuthBasicPlaceholder")}
"httpDestAuthBasicPlaceholder"
)}
value={ value={
cfg.basicCredentials ?? cfg.basicCredentials ??
"" ""
@@ -463,22 +448,16 @@ export function HttpDestinationCredenza({
htmlFor="auth-custom" htmlFor="auth-custom"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
{t( {t("httpDestAuthCustomTitle")}
"httpDestAuthCustomTitle"
)}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{t( {t("httpDestAuthCustomDescription")}
"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( placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
"httpDestAuthCustomHeaderNamePlaceholder"
)}
value={ value={
cfg.customHeaderName ?? cfg.customHeaderName ??
"" ""
@@ -493,9 +472,7 @@ export function HttpDestinationCredenza({
className="flex-1" className="flex-1"
/> />
<Input <Input
placeholder={t( placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
"httpDestAuthCustomHeaderValuePlaceholder"
)}
value={ value={
cfg.customHeaderValue ?? cfg.customHeaderValue ??
"" ""
@@ -616,14 +593,10 @@ export function HttpDestinationCredenza({
htmlFor="fmt-json-array" htmlFor="fmt-json-array"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
{t( {t("httpDestFormatJsonArrayTitle")}
"httpDestFormatJsonArrayTitle"
)}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{t( {t("httpDestFormatJsonArrayDescription")}
"httpDestFormatJsonArrayDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -643,9 +616,7 @@ 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( {t("httpDestFormatNdjsonDescription")}
"httpDestFormatNdjsonDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -665,9 +636,7 @@ 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( {t("httpDestFormatSingleDescription")}
"httpDestFormatSingleDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -748,9 +717,7 @@ 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( {t("httpDestConnectionLogsDescription")}
"httpDestConnectionLogsDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -772,9 +739,7 @@ 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( {t("httpDestRequestLogsDescription")}
"httpDestRequestLogsDescription"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -799,9 +764,7 @@ export function HttpDestinationCredenza({
loading={saving} loading={saving}
disabled={!isValid || saving} disabled={!isValid || saving}
> >
{editing {editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
? t("httpDestSaveChanges")
: t("httpDestCreateDestination")}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>

View File

@@ -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}&user=${email}` ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${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}&user=${email}` ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/login?redirect=/invite?token=${tokenParam}`; : `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl); router.push(redirectUrl);
} }

View File

@@ -16,7 +16,6 @@ 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;
@@ -53,7 +52,6 @@ export default async function OrgLoginPage({
const t = await getTranslations(); const t = await getTranslations();
return ( return (
<div> <div>
{build !== "enterprise" || !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")}{" "}
@@ -67,7 +65,6 @@ export default async function OrgLoginPage({
</Link> </Link>
</span> </span>
</div> </div>
) : null}
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardHeader> <CardHeader>
{branding?.logoUrl && ( {branding?.logoUrl && (

View File

@@ -96,7 +96,6 @@ 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({
@@ -571,14 +570,10 @@ export default function ProxyResourcesTable({
/> />
) : null} ) : null}
<div className=""> <div className="">
{!resourceRow.wildcard ? (
<CopyToClipboard <CopyToClipboard
text={resourceRow.domain} text={resourceRow.domain}
isLink={true} isLink={true}
/> />
) : (
<span>{resourceRow.domain}</span>
)}
</div> </div>
</div> </div>
); );

View File

@@ -375,8 +375,7 @@ 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")}{" "}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@@ -12,64 +12,13 @@ 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 { Input } from "@app/components/ui/input"; import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
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: Destination | null; editing: any;
orgId: string; orgId: string;
onSaved: () => void; onSaved: () => void;
} }
@@ -79,84 +28,18 @@ 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 ? t("S3DestEditTitle") : t("S3DestAddTitle")} {editing
? t("S3DestEditTitle")
: t("S3DestAddTitle")}
</CredenzaTitle> </CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
{editing {editing
@@ -166,375 +49,13 @@ export function S3DestinationCredenza({
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
{editing?.lastError && ( <ContactSalesBanner />
<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 <Button variant="outline">{t("cancel")}</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>

View File

@@ -61,8 +61,6 @@ export default function ShareLinksTable({
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedLink, setSelectedLink] = useState<ShareLinkRow | null>(null);
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks); const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
@@ -94,7 +92,6 @@ export default function ShareLinksTable({
title: t("shareErrorDelete"), title: t("shareErrorDelete"),
description: formatAxiosError(e, t("shareErrorDeleteMessage")) description: formatAxiosError(e, t("shareErrorDeleteMessage"))
}); });
throw e;
}); });
const newRows = rows.filter((r) => r.accessTokenId !== id); const newRows = rows.filter((r) => r.accessTokenId !== id);
@@ -296,10 +293,9 @@ export default function ShareLinksTable({
{/* </DropdownMenu> */} {/* </DropdownMenu> */}
<Button <Button
variant={"outline"} variant={"outline"}
onClick={() => { onClick={() =>
setSelectedLink(resourceRow); deleteSharelink(row.original.accessTokenId)
setIsDeleteModalOpen(true); }
}}
> >
{t("delete")} {t("delete")}
</Button> </Button>
@@ -311,30 +307,6 @@ export default function ShareLinksTable({
return ( return (
<> <>
{selectedLink && (
<ConfirmDeleteDialog
open={isDeleteModalOpen}
setOpen={(val) => {
setIsDeleteModalOpen(val);
if (!val) setSelectedLink(null);
}}
dialog={
<div className="space-y-2">
<p>{t("shareQuestionRemove")}</p>
<p>{t("shareMessageRemove")}</p>
</div>
}
buttonText={t("shareDeleteConfirm")}
onConfirm={async () =>
deleteSharelink(selectedLink.accessTokenId)
}
string={
selectedLink.title || selectedLink.resourceName
}
title={t("shareDelete")}
/>
)}
<CreateShareLinkForm <CreateShareLinkForm
open={isCreateModalOpen} open={isCreateModalOpen}
setOpen={setIsCreateModalOpen} setOpen={setIsCreateModalOpen}

View File

@@ -99,14 +99,6 @@ export default function UsersTable({
]; ];
}, [searchParams.toString()]); }, [searchParams.toString()]);
const isRemovingSelf = useMemo(() => {
if (!selectedUser || !user) return false;
return (
`${selectedUser.username}-${selectedUser.idpId}` ===
`${user.username}-${user.idpId}`
);
}, [selectedUser, user]);
function handleFilterChange( function handleFilterChange(
column: string, column: string,
value: string | undefined | null value: string | undefined | null
@@ -231,7 +223,10 @@ export default function UsersTable({
header: () => <span className="p-3"></span>, header: () => <span className="p-3"></span>,
cell: ({ row }) => { cell: ({ row }) => {
const userRow = row.original; const userRow = row.original;
const canRemoveFromOrg = !userRow.isOwner; const isCurrentUser =
`${userRow.username}-${userRow.idpId}` ===
`${user?.username}-${user?.idpId}`;
const isDisabled = userRow.isOwner || isCurrentUser;
return ( return (
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<div> <div>
@@ -240,6 +235,7 @@ export default function UsersTable({
<Button <Button
variant="ghost" variant="ghost"
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
disabled={isDisabled}
> >
<span className="sr-only"> <span className="sr-only">
{t("openMenu")} {t("openMenu")}
@@ -251,12 +247,16 @@ export default function UsersTable({
<Link <Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`} href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full" className="block w-full"
aria-disabled={isDisabled}
onClick={(e) =>
isDisabled && e.preventDefault()
}
> >
<DropdownMenuItem> <DropdownMenuItem disabled={isDisabled}>
{t("accessUserManage")} {t("accessUserManage")}
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
{canRemoveFromOrg && ( {!isDisabled && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@@ -271,6 +271,16 @@ export default function UsersTable({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{isDisabled ? (
<Button
variant={"outline"}
className="ml-2"
disabled
>
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
) : (
<Link <Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`} href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
> >
@@ -279,6 +289,7 @@ export default function UsersTable({
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>
</Link> </Link>
)}
</div> </div>
); );
} }
@@ -348,35 +359,14 @@ export default function UsersTable({
}} }}
dialog={ dialog={
<div className="space-y-2"> <div className="space-y-2">
<p> <p>{t("userQuestionOrgRemove")}</p>
{t( <p>{t("userMessageOrgRemove")}</p>
isRemovingSelf
? "userQuestionOrgRemoveSelf"
: "userQuestionOrgRemove"
)}
</p>
<p>
{t(
isRemovingSelf
? "userMessageOrgRemoveSelf"
: "userMessageOrgRemove"
)}
</p>
</div> </div>
} }
buttonText={t( buttonText={t("userRemoveOrgConfirm")}
isRemovingSelf
? "userRemoveOrgConfirmSelf"
: "userRemoveOrgConfirm"
)}
warningText={
isRemovingSelf ? t("userRemoveOrgSelfWarning") : undefined
}
onConfirm={async () => startTransition(removeUser)} onConfirm={async () => startTransition(removeUser)}
string={ string={
isRemovingSelf selectedUser
? t("userRemoveOrgConfirmPhraseSelf")
: selectedUser
? getUserDisplayName({ ? getUserDisplayName({
email: selectedUser.email, email: selectedUser.email,
name: selectedUser.name, name: selectedUser.name,
@@ -384,9 +374,7 @@ export default function UsersTable({
}) })
: "" : ""
} }
title={t( title={t("userRemoveOrg")}
isRemovingSelf ? "userRemoveOrgSelf" : "userRemoveOrg"
)}
/> />
<ControlledDataTable <ControlledDataTable

View File

@@ -11,7 +11,7 @@ import { cn } from "@app/lib/cn";
import { CheckIcon } from "lucide-react"; import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
export type TagValue = { text: string; id: string; isAdmin?: boolean }; export type TagValue = { text: string; id: string };
export type MultiSelectTagsProps<T extends TagValue> = { export type MultiSelectTagsProps<T extends TagValue> = {
emptyPlaceholder?: string; emptyPlaceholder?: string;

View File

@@ -6,7 +6,7 @@ import { useDebounce } from "use-debounce";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input"; import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
export type SelectedRole = { id: string; text: string; isAdmin?: boolean }; export type SelectedRole = { id: string; text: string };
export type RolesSelectorProps = { export type RolesSelectorProps = {
orgId: string; orgId: string;

View File

@@ -81,8 +81,6 @@ export function pullEnv(): Env {
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER === "true" process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER === "true"
? true ? true
: false, : false,
hidePoweredBy:
process.env.BRANDING_HIDE_POWERED_BY === "true" ? true : false,
logo: { logo: {
lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string, lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string,
darkPath: process.env.BRANDING_LOGO_DARK_PATH as string, darkPath: process.env.BRANDING_LOGO_DARK_PATH as string,

View File

@@ -41,7 +41,6 @@ export type Env = {
appName?: string; appName?: string;
background_image_path?: string; background_image_path?: string;
hideAuthLayoutFooter?: boolean; hideAuthLayoutFooter?: boolean;
hidePoweredBy?: boolean;
logo?: { logo?: {
lightPath?: string; lightPath?: string;
darkPath?: string; darkPath?: string;