mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-21 20:36:37 +00:00
Compare commits
55 Commits
cloud-mult
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63694032e8 | ||
|
|
b77aaedb58 | ||
|
|
a316d0301f | ||
|
|
dcd499720e | ||
|
|
e18fe21eca | ||
|
|
2970b51fb8 | ||
|
|
b9236ff52e | ||
|
|
38eb0ec7ed | ||
|
|
ecba4a0b80 | ||
|
|
e6da18c952 | ||
|
|
12941ac5ae | ||
|
|
11085bda63 | ||
|
|
c03211cc53 | ||
|
|
2867459600 | ||
|
|
32b24db9bf | ||
|
|
660bf9ff87 | ||
|
|
78c4ddebba | ||
|
|
f2dfadb37b | ||
|
|
3f2bdf081f | ||
|
|
d6ba34aeea | ||
|
|
6442eb12fb | ||
|
|
01c15afa74 | ||
|
|
4e88f1f38a | ||
|
|
13ab505f4d | ||
|
|
7d112aab27 | ||
|
|
eedf57af89 | ||
|
|
7a01a4e090 | ||
|
|
874794c996 | ||
|
|
5e37c4e85f | ||
|
|
4e7eac368f | ||
|
|
756f3f32ca | ||
|
|
362981ad19 | ||
|
|
fa4f7e4ac2 | ||
|
|
c6bca4e2ab | ||
|
|
e28b361e05 | ||
|
|
a18691011b | ||
|
|
c4a6403cba | ||
|
|
1851bf941a | ||
|
|
b7ab3c2e92 | ||
|
|
ce1ad032ba | ||
|
|
8446c68e1b | ||
|
|
40ed388b0f | ||
|
|
ce1693aa2f | ||
|
|
11d16a1552 | ||
|
|
0ac54a2c88 | ||
|
|
b7d8b32123 | ||
|
|
5987f6b2cd | ||
|
|
7ad76f5683 | ||
|
|
09a9457021 | ||
|
|
ca4643ec36 | ||
|
|
e2f78ba476 | ||
|
|
5d92190d50 | ||
|
|
8315d4b6ae | ||
|
|
9eacefb155 | ||
|
|
843b13ed57 |
39
.github/workflows/cicd.yml
vendored
39
.github/workflows/cicd.yml
vendored
@@ -525,41 +525,10 @@ jobs:
|
||||
VERIFIED_INDEX_KEYLESS=false
|
||||
fi
|
||||
|
||||
# If index verification fails, attempt to verify child platform manifests
|
||||
if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||
echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
CHILD_VERIFIED=false
|
||||
|
||||
for ARCH in arm64 amd64; do
|
||||
CHILD_TAG="${IMAGE_TAG}-${ARCH}"
|
||||
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
|
||||
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
|
||||
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
|
||||
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
|
||||
echo "==> cosign verify (public key) child ${CHILD_REF}"
|
||||
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
|
||||
CHILD_VERIFIED=true
|
||||
echo "Public key verification succeeded for child ${CHILD_REF}"
|
||||
else
|
||||
echo "Public key verification failed for child ${CHILD_REF}"
|
||||
fi
|
||||
|
||||
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
|
||||
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
|
||||
CHILD_VERIFIED=true
|
||||
echo "Keyless verification succeeded for child ${CHILD_REF}"
|
||||
else
|
||||
echo "Keyless verification failed for child ${CHILD_REF}"
|
||||
fi
|
||||
else
|
||||
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${CHILD_VERIFIED}" != "true" ]; then
|
||||
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
exit 1
|
||||
fi
|
||||
# Check if verification succeeded
|
||||
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||
fi
|
||||
) || TAG_FAILED=true
|
||||
|
||||
|
||||
121
cli/commands/generateOrgCaKeys.ts
Normal file
121
cli/commands/generateOrgCaKeys.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { generateCA } from "@server/private/lib/sshCA";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type GenerateOrgCaKeysArgs = {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
|
||||
command: "generate-org-ca-keys",
|
||||
describe:
|
||||
"Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("secret", {
|
||||
type: "string",
|
||||
describe:
|
||||
"Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
|
||||
})
|
||||
.option("force", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe:
|
||||
"Overwrite existing CA keys for the org if they already exist"
|
||||
});
|
||||
},
|
||||
handler: async (argv: {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { orgId, force } = argv;
|
||||
let secret = argv.secret;
|
||||
|
||||
if (!secret) {
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
"Error: No server secret provided and config file not found. " +
|
||||
"Expected config.yml or config.yaml in the config directory, or pass --secret."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as {
|
||||
server?: { secret?: string };
|
||||
};
|
||||
|
||||
if (!config?.server?.secret) {
|
||||
console.error(
|
||||
"Error: No server.secret in config file. Pass --secret or set server.secret in config."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
secret = config.server.secret;
|
||||
}
|
||||
|
||||
const [org] = await db
|
||||
.select({
|
||||
orgId: orgs.orgId,
|
||||
sshCaPrivateKey: orgs.sshCaPrivateKey,
|
||||
sshCaPublicKey: orgs.sshCaPublicKey
|
||||
})
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
console.error(`Error: Organization with orgId "${orgId}" not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
|
||||
if (!force) {
|
||||
console.error(
|
||||
"Error: This organization already has CA keys. Use --force to overwrite."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||
|
||||
await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
sshCaPrivateKey: encryptedPrivateKey,
|
||||
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
console.log("SSH CA keys generated and stored for org:", orgId);
|
||||
console.log("\nPublic key (OpenSSH format):");
|
||||
console.log(ca.publicKeyOpenSSH);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error generating org CA keys:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { clearExitNodes } from "./commands/clearExitNodes";
|
||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||
import { deleteClient } from "./commands/deleteClient";
|
||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
@@ -17,5 +18,6 @@ yargs(hideBin(process.argv))
|
||||
.command(rotateServerSecret)
|
||||
.command(clearLicenseKeys)
|
||||
.command(deleteClient)
|
||||
.command(generateOrgCaKeys)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Изберете протокол",
|
||||
"resourcePortNumber": "Номер на порт",
|
||||
"resourcePortNumberDescription": "Външен номер на порт за прокси заявки.",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмяна",
|
||||
"resourceConfig": "Конфигурационни фрагменти",
|
||||
"resourceConfigDescription": "Копирайте и поставете тези конфигурационни отрязъци, за да настроите TCP/UDP ресурса",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Възникна грешка при изтриването на организацията.",
|
||||
"orgDeleted": "Организацията е изтрита",
|
||||
"orgDeletedMessage": "Организацията и нейните данни са изтрити.",
|
||||
"deleteAccount": "Изтриване на профил",
|
||||
"deleteAccountDescription": "Перманентно изтрийте своя профил, всички организации, които притежавате, и всички данни в тези организации. Това не може да бъде отменено.",
|
||||
"deleteAccountButton": "Изтриване на профил",
|
||||
"deleteAccountConfirmTitle": "Изтрий профила",
|
||||
"deleteAccountConfirmMessage": "Това ще изтрие перманентно вашия профил, всички организации, които притежавате, и всички данни в тези организации. Това не може да бъде отменено.",
|
||||
"deleteAccountConfirmString": "изтриване на профил",
|
||||
"deleteAccountSuccess": "Профилът е изтрит",
|
||||
"deleteAccountSuccessMessage": "Вашият профил е изтрит.",
|
||||
"deleteAccountError": "Неуспешно изтриване на профил",
|
||||
"deleteAccountPreviewAccount": "Вашият профил",
|
||||
"deleteAccountPreviewOrgs": "Организации, които притежавате (и всички техни данни)",
|
||||
"orgMissing": "Липсва идентификатор на организация",
|
||||
"orgMissingMessage": "Невъзможност за регенериране на покана без идентификатор на организация.",
|
||||
"accessUsersManage": "Управление на потребители",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Филтрирайте по състояние на одобрение",
|
||||
"approvalListEmpty": "Няма одобрения",
|
||||
"approvalState": "Състояние на одобрение",
|
||||
"approvalLoadMore": "Заредете още",
|
||||
"loadingApprovals": "Зарежда се одобрение",
|
||||
"approve": "Одобряване",
|
||||
"approved": "Одобрен",
|
||||
"denied": "Отказан",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Ролята е премахната",
|
||||
"accessRoleRemovedDescription": "Ролята беше успешно премахната.",
|
||||
"accessRoleRequiredRemove": "Преди да изтриете тази роля, моля изберете нова роля, към която да прехвърлите настоящите членове.",
|
||||
"network": "Мрежа",
|
||||
"manage": "Управление",
|
||||
"sitesNotFound": "Няма намерени сайтове.",
|
||||
"pangolinServerAdmin": "Администратор на сървър - Панголин",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Настройка - Pangolin",
|
||||
"orgNameRequired": "Името на организацията е задължително",
|
||||
"orgIdRequired": "ID на организацията е задължително",
|
||||
"orgIdMaxLength": "ID на организация трябва да бъде най-много 32 символа",
|
||||
"orgErrorCreate": "Възникна грешка при създаване на организация",
|
||||
"pageNotFound": "Страницата не е намерена",
|
||||
"pageNotFoundDescription": "О, не! Страницата, която търсите, не съществува.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Преглед на дневници",
|
||||
"noneSelected": "Нищо не е избрано",
|
||||
"orgNotFound2": "Няма намерени организации.",
|
||||
"searchProgress": "Търсене...",
|
||||
"searchPlaceholder": "Търсене...",
|
||||
"emptySearchOptions": "Няма намерени опции",
|
||||
"create": "Създаване",
|
||||
"orgs": "Организации",
|
||||
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Частно",
|
||||
"sidebarAccessControl": "Контрол на достъпа",
|
||||
"sidebarLogsAndAnalytics": "Дневници и анализи",
|
||||
"sidebarTeam": "Екип",
|
||||
"sidebarUsers": "Потребители",
|
||||
"sidebarAdmin": "Администратор",
|
||||
"sidebarInvitations": "Покани",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Лог & Анализи",
|
||||
"sidebarBluePrints": "Чертежи",
|
||||
"sidebarOrganization": "Организация",
|
||||
"sidebarManagement": "Управление",
|
||||
"sidebarBillingAndLicenses": "Фактуриране & Лицензи",
|
||||
"sidebarLogsAnalytics": "Анализи",
|
||||
"blueprints": "Чертежи",
|
||||
"blueprintsDescription": "Прилагайте декларативни конфигурации и преглеждайте предишни изпълнения",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Парсирано съдържание (само за четене)",
|
||||
"enableDockerSocket": "Активиране на Docker Чернова",
|
||||
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
|
||||
"enableDockerSocketLink": "Научете повече",
|
||||
"viewDockerContainers": "Преглед на Docker контейнери",
|
||||
"containersIn": "Контейнери в {siteName}",
|
||||
"selectContainerDescription": "Изберете контейнер, който да ползвате като име на хост за целта. Натиснете порт, за да ползвате порт",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Сайтове",
|
||||
"billingUsers": "Потребители",
|
||||
"billingDomains": "Домейни",
|
||||
"billingOrganizations": "Организации",
|
||||
"billingRemoteExitNodes": "Дистанционни възли",
|
||||
"billingNoLimitConfigured": "Няма конфигуриран лимит",
|
||||
"billingEstimatedPeriod": "Очакван период на фактуриране",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Неуспешно",
|
||||
"createNewOrgDescription": "Създайте нова организация",
|
||||
"organization": "Организация",
|
||||
"primary": "Основно",
|
||||
"port": "Порт",
|
||||
"securityKeyManage": "Управление на ключове за защита",
|
||||
"securityKeyDescription": "Добавяне или премахване на ключове за защита за удостоверяване без парола",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Времето е в секунди",
|
||||
"requireDeviceApproval": "Изискват одобрение на устройства",
|
||||
"requireDeviceApprovalDescription": "Потребители с тази роля трябва да имат нови устройства одобрени от администратор преди да могат да се свържат и да имат достъп до ресурси.",
|
||||
"sshAccess": "SSH достъп",
|
||||
"roleAllowSsh": "Разреши SSH",
|
||||
"roleAllowSshAllow": "Разреши",
|
||||
"roleAllowSshDisallow": "Забрани",
|
||||
"roleAllowSshDescription": "Разреши на потребителите с тази роля да се свързват с ресурси чрез SSH. Когато е деактивирано, ролята не може да използва SSH достъп.",
|
||||
"sshSudoMode": "Sudo достъп",
|
||||
"sshSudoModeNone": "Няма",
|
||||
"sshSudoModeNoneDescription": "Потребителят не може да изпълнява команди с sudo.",
|
||||
"sshSudoModeFull": "Пълен Sudo",
|
||||
"sshSudoModeFullDescription": "Потребителят може да изпълнява всяка команда с sudo.",
|
||||
"sshSudoModeCommands": "Команди",
|
||||
"sshSudoModeCommandsDescription": "Потребителят може да изпълнява само определени команди с sudo.",
|
||||
"sshSudo": "Разреши sudo",
|
||||
"sshSudoCommands": "Sudo команди",
|
||||
"sshSudoCommandsDescription": "Списък с команди, които потребителят е разрешено да изпълнява с sudo.",
|
||||
"sshCreateHomeDir": "Създай начална директория",
|
||||
"sshUnixGroups": "Unix групи",
|
||||
"sshUnixGroupsDescription": "Unix групи, в които да добавите потребителя на целевия хост.",
|
||||
"retryAttempts": "Опити за повторно",
|
||||
"expectedResponseCodes": "Очаквани кодове за отговор",
|
||||
"expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?",
|
||||
"authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.",
|
||||
"brandingLogoURL": "URL адрес на логото.",
|
||||
"brandingLogoURLOrPath": "URL или Път към лого",
|
||||
"brandingLogoPathDescription": "Въведете URL или локален път.",
|
||||
"brandingLogoURLDescription": "Въведете публично достъпен URL към вашето лого изображение.",
|
||||
"brandingPrimaryColor": "Основен цвят.",
|
||||
"brandingLogoWidth": "Ширина (px).",
|
||||
"brandingLogoHeight": "Височина (px).",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Контрол на достъпа.",
|
||||
"editInternalResourceDialogAccessControlDescription": "Контролирайте кои роли, потребители и клиентски машини имат достъп до този ресурс, когато са свързани. Администраторите винаги имат достъп.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Обхватът на портовете трябва да е \"*\" за всички портове или списък от разделени със запетая портове и диапазони (например: \"80,443,8000-9000\"). Портовете трябва да са между 1 и 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Локация на SSH Auth Daemon",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Изберете къде ще работи демонът за SSH удостоверение: на сайта (Newt) или на отдалечен хост.",
|
||||
"internalResourceAuthDaemonDescription": "Демонът за SSH удостоверение управлява подписването на SSH ключове и PAM удостоверение за този ресурс. Изберете дали да работи на сайта (Newt) или на отделен отдалечен хост. Вижте <docsLink>документацията</docsLink> за повече информация.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Изберете стратегия",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Местоположение",
|
||||
"internalResourceAuthDaemonSite": "На сайта",
|
||||
"internalResourceAuthDaemonSiteDescription": "Демонът за удостоверение работи на сайта (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Отдалечен хост",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Демонът за удостоверение работи на хост, който не е сайтът.",
|
||||
"internalResourceAuthDaemonPort": "Порт на демона (незадължителен)",
|
||||
"orgAuthWhatsThis": "Къде мога да намеря идентификатора на организацията си?",
|
||||
"learnMore": "Научете повече.",
|
||||
"backToHome": "Връщане към началната страница.",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Vybrat protokol",
|
||||
"resourcePortNumber": "Číslo portu",
|
||||
"resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.",
|
||||
"back": "Zpět",
|
||||
"cancel": "Zrušit",
|
||||
"resourceConfig": "Konfigurační snippety",
|
||||
"resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační textové bloky pro nastavení TCP/UDP zdroje",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Došlo k chybě při odstraňování organizace.",
|
||||
"orgDeleted": "Organizace odstraněna",
|
||||
"orgDeletedMessage": "Organizace a její data byla smazána.",
|
||||
"deleteAccount": "Odstranit účet",
|
||||
"deleteAccountDescription": "Trvale smazat svůj účet, všechny organizace, které vlastníte, a všechna data těchto organizací. Tuto akci nelze vrátit zpět.",
|
||||
"deleteAccountButton": "Odstranit účet",
|
||||
"deleteAccountConfirmTitle": "Odstranit účet",
|
||||
"deleteAccountConfirmMessage": "Toto trvale vymaže váš účet, všechny organizace, které vlastníte, a všechna data v rámci těchto organizací. Tuto akci nelze vrátit zpět.",
|
||||
"deleteAccountConfirmString": "smazat účet",
|
||||
"deleteAccountSuccess": "Účet odstraněn",
|
||||
"deleteAccountSuccessMessage": "Váš účet byl odstraněn.",
|
||||
"deleteAccountError": "Nepodařilo se odstranit účet",
|
||||
"deleteAccountPreviewAccount": "Váš účet",
|
||||
"deleteAccountPreviewOrgs": "Organizace, které vlastníte (a všechny jejich údaje)",
|
||||
"orgMissing": "Chybí ID organizace",
|
||||
"orgMissingMessage": "Nelze obnovit pozvánku bez ID organizace.",
|
||||
"accessUsersManage": "Spravovat uživatele",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtrovat podle státu schválení",
|
||||
"approvalListEmpty": "Žádná schválení",
|
||||
"approvalState": "Země schválení",
|
||||
"approvalLoadMore": "Načíst více",
|
||||
"loadingApprovals": "Načítání schválení",
|
||||
"approve": "Schválit",
|
||||
"approved": "Schváleno",
|
||||
"denied": "Zamítnuto",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Role odstraněna",
|
||||
"accessRoleRemovedDescription": "Role byla úspěšně odstraněna.",
|
||||
"accessRoleRequiredRemove": "Před odstraněním této role vyberte novou roli, do které chcete převést existující členy.",
|
||||
"network": "Síť",
|
||||
"manage": "Spravovat",
|
||||
"sitesNotFound": "Nebyly nalezeny žádné stránky.",
|
||||
"pangolinServerAdmin": "Správce serveru - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Setup - Pangolin",
|
||||
"orgNameRequired": "Je vyžadován název organizace",
|
||||
"orgIdRequired": "Je vyžadováno ID organizace",
|
||||
"orgIdMaxLength": "ID organizace musí mít nejvýše 32 znaků",
|
||||
"orgErrorCreate": "Při vytváření org došlo k chybě",
|
||||
"pageNotFound": "Stránka nenalezena",
|
||||
"pageNotFoundDescription": "Jejda! Stránka, kterou hledáte, neexistuje.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Zobrazit logy",
|
||||
"noneSelected": "Není vybráno",
|
||||
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
|
||||
"searchProgress": "Hledat...",
|
||||
"searchPlaceholder": "Hledat...",
|
||||
"emptySearchOptions": "Nebyly nalezeny žádné možnosti",
|
||||
"create": "Vytvořit",
|
||||
"orgs": "Organizace",
|
||||
"loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Soukromé",
|
||||
"sidebarAccessControl": "Kontrola přístupu",
|
||||
"sidebarLogsAndAnalytics": "Logy & Analytika",
|
||||
"sidebarTeam": "Tým",
|
||||
"sidebarUsers": "Uživatelé",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Pozvánky",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Plány",
|
||||
"sidebarOrganization": "Organizace",
|
||||
"sidebarManagement": "Správa",
|
||||
"sidebarBillingAndLicenses": "Fakturace a licence",
|
||||
"sidebarLogsAnalytics": "Analytici",
|
||||
"blueprints": "Plány",
|
||||
"blueprintsDescription": "Použít deklarativní konfigurace a zobrazit předchozí běhy",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Parsed content (Pouze pro čtení)",
|
||||
"enableDockerSocket": "Povolit Docker plán",
|
||||
"enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.",
|
||||
"enableDockerSocketLink": "Zjistit více",
|
||||
"viewDockerContainers": "Zobrazit kontejnery Dockeru",
|
||||
"containersIn": "Kontejnery v {siteName}",
|
||||
"selectContainerDescription": "Vyberte jakýkoli kontejner pro použití jako název hostitele pro tento cíl. Klikněte na port pro použití portu.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Stránky",
|
||||
"billingUsers": "Uživatelé",
|
||||
"billingDomains": "Domény",
|
||||
"billingOrganizations": "Tělo",
|
||||
"billingRemoteExitNodes": "Vzdálené uzly",
|
||||
"billingNoLimitConfigured": "Žádný limit nenastaven",
|
||||
"billingEstimatedPeriod": "Odhadované období fakturace",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Selhalo",
|
||||
"createNewOrgDescription": "Vytvořit novou organizaci",
|
||||
"organization": "Organizace",
|
||||
"primary": "Primární",
|
||||
"port": "Přístav",
|
||||
"securityKeyManage": "Správa bezpečnostních klíčů",
|
||||
"securityKeyDescription": "Přidat nebo odebrat bezpečnostní klíče pro bezheslou autentizaci",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Čas je v sekundách",
|
||||
"requireDeviceApproval": "Vyžadovat schválení zařízení",
|
||||
"requireDeviceApprovalDescription": "Uživatelé s touto rolí potřebují nová zařízení schválená správcem, než se mohou připojit a přistupovat ke zdrojům.",
|
||||
"sshAccess": "SSH přístup",
|
||||
"roleAllowSsh": "Povolit SSH",
|
||||
"roleAllowSshAllow": "Povolit",
|
||||
"roleAllowSshDisallow": "Zakázat",
|
||||
"roleAllowSshDescription": "Povolit uživatelům s touto rolí připojení k zdrojům přes SSH. Je-li zakázáno, role nemůže používat přístup SSH.",
|
||||
"sshSudoMode": "Súdánský přístup",
|
||||
"sshSudoModeNone": "Nic",
|
||||
"sshSudoModeNoneDescription": "Uživatel nemůže spouštět příkazy se sudo.",
|
||||
"sshSudoModeFull": "Úplný Súdán",
|
||||
"sshSudoModeFullDescription": "Uživatel může spustit libovolný příkaz se sudo.",
|
||||
"sshSudoModeCommands": "Příkazy",
|
||||
"sshSudoModeCommandsDescription": "Uživatel může spustit pouze zadané příkazy s sudo.",
|
||||
"sshSudo": "Povolit sudo",
|
||||
"sshSudoCommands": "Sudo příkazy",
|
||||
"sshSudoCommandsDescription": "Seznam příkazů, které může uživatel spouštět s sudo.",
|
||||
"sshCreateHomeDir": "Vytvořit domovský adresář",
|
||||
"sshUnixGroups": "Unixové skupiny",
|
||||
"sshUnixGroupsDescription": "Unix skupiny přidají uživatele do cílového hostitele.",
|
||||
"retryAttempts": "Opakovat pokusy",
|
||||
"expectedResponseCodes": "Očekávané kódy odezvy",
|
||||
"expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?",
|
||||
"authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu",
|
||||
"brandingLogoURL": "URL loga",
|
||||
"brandingLogoURLOrPath": "URL nebo cesta k logu",
|
||||
"brandingLogoPathDescription": "Zadejte URL nebo místní cestu.",
|
||||
"brandingLogoURLDescription": "Zadejte veřejně přístupnou adresu URL vašeho loga.",
|
||||
"brandingPrimaryColor": "Primární barva",
|
||||
"brandingLogoWidth": "Šířka (px)",
|
||||
"brandingLogoHeight": "Výška (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Řízení přístupu",
|
||||
"editInternalResourceDialogAccessControlDescription": "Kontrolujte, které role, uživatelé a klienti mohou přistupovat k tomuto prostředku, když jsou připojeni. Admini mají vždy přístup.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Rozsah portů musí být \"*\" pro všechny porty, nebo seznam portů a rozsahů oddělených čárkou (např. \"80,443,8000-9000\"). Porty musí být mezi 1 a 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Démon umístění",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Zvolte, kde běží SSH autentizační démon: na stránce (Newt) nebo na vzdáleném serveru.",
|
||||
"internalResourceAuthDaemonDescription": "SSH autentizační daemon zpracovává podpis SSH klíče a PAM autentizaci tohoto zdroje. Vyberte si, zda běží na webu (Newt) nebo na samostatném vzdáleném serveru. Více informací najdete v <docsLink>dokumentaci</docsLink>.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Vybrat strategii",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Poloha",
|
||||
"internalResourceAuthDaemonSite": "Na stránce",
|
||||
"internalResourceAuthDaemonSiteDescription": "Auth daemon běží na webu (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Vzdálený server",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Auth daemon běží na hostitele, který není web.",
|
||||
"internalResourceAuthDaemonPort": "Daemon port (volitelné)",
|
||||
"orgAuthWhatsThis": "Kde najdu ID mé organizace?",
|
||||
"learnMore": "Zjistit více",
|
||||
"backToHome": "Zpět na domovskou stránku",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Wählen Sie ein Protokoll",
|
||||
"resourcePortNumber": "Portnummer",
|
||||
"resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.",
|
||||
"back": "Zurück",
|
||||
"cancel": "Abbrechen",
|
||||
"resourceConfig": "Konfiguration Snippets",
|
||||
"resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um die TCP/UDP Ressource einzurichten",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Beim Löschen der Organisation ist ein Fehler aufgetreten.",
|
||||
"orgDeleted": "Organisation gelöscht",
|
||||
"orgDeletedMessage": "Die Organisation und ihre Daten wurden gelöscht.",
|
||||
"deleteAccount": "Konto löschen",
|
||||
"deleteAccountDescription": "Lösche dein Konto, alle Organisationen, die du besitzt, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleteAccountButton": "Konto löschen",
|
||||
"deleteAccountConfirmTitle": "Konto löschen",
|
||||
"deleteAccountConfirmMessage": "Dies wird Ihr Konto dauerhaft löschen, alle Organisationen, die Sie besitzen, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleteAccountConfirmString": "Konto löschen",
|
||||
"deleteAccountSuccess": "Konto gelöscht",
|
||||
"deleteAccountSuccessMessage": "Ihr Konto wurde gelöscht.",
|
||||
"deleteAccountError": "Konto konnte nicht gelöscht werden",
|
||||
"deleteAccountPreviewAccount": "Ihr Konto",
|
||||
"deleteAccountPreviewOrgs": "Organisationen, die Sie besitzen (und ihre Daten)",
|
||||
"orgMissing": "Organisations-ID fehlt",
|
||||
"orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.",
|
||||
"accessUsersManage": "Benutzer verwalten",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtern nach Genehmigungsstatus",
|
||||
"approvalListEmpty": "Keine Genehmigungen",
|
||||
"approvalState": "Genehmigungsstatus",
|
||||
"approvalLoadMore": "Mehr laden",
|
||||
"loadingApprovals": "Genehmigungen werden geladen",
|
||||
"approve": "Bestätigen",
|
||||
"approved": "Genehmigt",
|
||||
"denied": "Verweigert",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rolle entfernt",
|
||||
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
|
||||
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
|
||||
"network": "Netzwerk",
|
||||
"manage": "Verwalten",
|
||||
"sitesNotFound": "Keine Standorte gefunden.",
|
||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Einrichtung - Pangolin",
|
||||
"orgNameRequired": "Organisationsname ist erforderlich",
|
||||
"orgIdRequired": "Organisations-ID ist erforderlich",
|
||||
"orgIdMaxLength": "Organisations-ID darf höchstens 32 Zeichen lang sein",
|
||||
"orgErrorCreate": "Beim Erstellen der Organisation ist ein Fehler aufgetreten",
|
||||
"pageNotFound": "Seite nicht gefunden",
|
||||
"pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Logs anzeigen",
|
||||
"noneSelected": "Keine ausgewählt",
|
||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||
"searchProgress": "Suche...",
|
||||
"searchPlaceholder": "Suche...",
|
||||
"emptySearchOptions": "Keine Optionen gefunden",
|
||||
"create": "Erstellen",
|
||||
"orgs": "Organisationen",
|
||||
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privat",
|
||||
"sidebarAccessControl": "Zugriffskontrolle",
|
||||
"sidebarLogsAndAnalytics": "Protokolle & Analysen",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Benutzer",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Einladungen",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Log & Analytik",
|
||||
"sidebarBluePrints": "Blaupausen",
|
||||
"sidebarOrganization": "Organisation",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
|
||||
"sidebarLogsAnalytics": "Analytik",
|
||||
"blueprints": "Blaupausen",
|
||||
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Analysierte Inhalte (Nur lesen)",
|
||||
"enableDockerSocket": "Docker Blueprint aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blueprintbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
"enableDockerSocketLink": "Mehr erfahren",
|
||||
"viewDockerContainers": "Docker Container anzeigen",
|
||||
"containersIn": "Container in {siteName}",
|
||||
"selectContainerDescription": "Wählen Sie einen Container, der als Hostname für dieses Ziel verwendet werden soll. Klicken Sie auf einen Port, um einen Port zu verwenden.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Seiten",
|
||||
"billingUsers": "Benutzergeräte",
|
||||
"billingDomains": "Domänen",
|
||||
"billingOrganizations": "Orden",
|
||||
"billingRemoteExitNodes": "Entfernte Knoten",
|
||||
"billingNoLimitConfigured": "Kein Limit konfiguriert",
|
||||
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Fehlgeschlagen",
|
||||
"createNewOrgDescription": "Eine neue Organisation erstellen",
|
||||
"organization": "Organisation",
|
||||
"primary": "Primär",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Sicherheitsschlüssel verwalten",
|
||||
"securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Zeit ist in Sekunden",
|
||||
"requireDeviceApproval": "Gerätegenehmigungen erforderlich",
|
||||
"requireDeviceApprovalDescription": "Benutzer mit dieser Rolle benötigen neue Geräte, die von einem Administrator genehmigt wurden, bevor sie sich verbinden und auf Ressourcen zugreifen können.",
|
||||
"sshAccess": "SSH-Zugriff",
|
||||
"roleAllowSsh": "SSH erlauben",
|
||||
"roleAllowSshAllow": "Erlauben",
|
||||
"roleAllowSshDisallow": "Nicht zulassen",
|
||||
"roleAllowSshDescription": "Benutzern mit dieser Rolle erlauben, sich über SSH mit Ressourcen zu verbinden. Wenn deaktiviert, kann die Rolle keinen SSH-Zugriff verwenden.",
|
||||
"sshSudoMode": "Sudo-Zugriff",
|
||||
"sshSudoModeNone": "Keine",
|
||||
"sshSudoModeNoneDescription": "Benutzer kann keine Befehle mit sudo ausführen.",
|
||||
"sshSudoModeFull": "Volles Sudo",
|
||||
"sshSudoModeFullDescription": "Benutzer kann jeden Befehl mit sudo ausführen.",
|
||||
"sshSudoModeCommands": "Befehle",
|
||||
"sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.",
|
||||
"sshSudo": "sudo erlauben",
|
||||
"sshSudoCommands": "Sudo-Befehle",
|
||||
"sshSudoCommandsDescription": "Liste der Befehle, die der Benutzer mit sudo ausführen darf.",
|
||||
"sshCreateHomeDir": "Home-Verzeichnis erstellen",
|
||||
"sshUnixGroups": "Unix-Gruppen",
|
||||
"sshUnixGroupsDescription": "Unix-Gruppen, zu denen der Benutzer auf dem Ziel-Host hinzugefügt wird.",
|
||||
"retryAttempts": "Wiederholungsversuche",
|
||||
"expectedResponseCodes": "Erwartete Antwortcodes",
|
||||
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
|
||||
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "Logo-URL oder Pfad",
|
||||
"brandingLogoPathDescription": "Geben Sie eine URL oder einen lokalen Pfad ein.",
|
||||
"brandingLogoURLDescription": "Geben Sie eine öffentlich zugängliche URL zu Ihrem Logobild ein.",
|
||||
"brandingPrimaryColor": "Primär-Farbe",
|
||||
"brandingLogoWidth": "Breite (px)",
|
||||
"brandingLogoHeight": "Höhe (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Zugriffskontrolle",
|
||||
"editInternalResourceDialogAccessControlDescription": "Kontrollieren Sie, welche Rollen, Benutzer und Maschinen-Clients Zugriff auf diese Ressource haben, wenn sie verbunden sind. Admins haben immer Zugriff.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Der Port-Bereich muss \"*\" für alle Ports sein, oder eine kommaseparierte Liste von Ports und Bereichen (z.B. \"80,443.8000-9000\"). Ports müssen zwischen 1 und 65535 liegen.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth-Daemon Standort",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Wählen Sie aus, wo der SSH-Authentifizierungs-Daemon läuft: auf der Site (Newt) oder auf einem entfernten Host.",
|
||||
"internalResourceAuthDaemonDescription": "Der SSH-Authentifizierungs-Daemon verarbeitet SSH-Schlüsselsignaturen und PAM-Authentifizierung für diese Ressource. Wählen Sie, ob sie auf der Website (Newt) oder auf einem separaten entfernten Host ausgeführt wird. Siehe <docsLink>die Dokumentation</docsLink> für mehr.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Strategie auswählen",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Standort",
|
||||
"internalResourceAuthDaemonSite": "Vor Ort",
|
||||
"internalResourceAuthDaemonSiteDescription": "Der Auth Daemon läuft auf der Seite (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Entfernter Host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Der Auth Daemon läuft auf einem Host, der nicht die Site ist.",
|
||||
"internalResourceAuthDaemonPort": "Daemon-Port (optional)",
|
||||
"orgAuthWhatsThis": "Wo finde ich meine Organisations-ID?",
|
||||
"learnMore": "Mehr erfahren",
|
||||
"backToHome": "Zurück zur Startseite",
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
"resourcesUsersRolesAccess": "User and role-based access control",
|
||||
"resourcesErrorUpdate": "Failed to toggle resource",
|
||||
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
|
||||
"access": "Access",
|
||||
"access": "Access Control",
|
||||
"shareLink": "{resource} Share Link",
|
||||
"resourceSelect": "Select resource",
|
||||
"shareLinks": "Share Links",
|
||||
@@ -790,6 +790,7 @@
|
||||
"accessRoleRemoved": "Role removed",
|
||||
"accessRoleRemovedDescription": "The role has been successfully removed.",
|
||||
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
|
||||
"network": "Network",
|
||||
"manage": "Manage",
|
||||
"sitesNotFound": "No sites found.",
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
@@ -1249,6 +1250,7 @@
|
||||
"sidebarClientResources": "Private",
|
||||
"sidebarAccessControl": "Access Control",
|
||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Users",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Invitations",
|
||||
@@ -1267,6 +1269,7 @@
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Blueprints",
|
||||
"sidebarOrganization": "Organization",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||
"sidebarLogsAnalytics": "Analytics",
|
||||
"blueprints": "Blueprints",
|
||||
@@ -1288,8 +1291,7 @@
|
||||
"contents": "Contents",
|
||||
"parsedContents": "Parsed Contents (Read Only)",
|
||||
"enableDockerSocket": "Enable Docker Blueprint",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||
"enableDockerSocketLink": "Learn More",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
||||
"viewDockerContainers": "View Docker Containers",
|
||||
"containersIn": "Containers in {siteName}",
|
||||
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
||||
@@ -1643,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Time is in seconds",
|
||||
"requireDeviceApproval": "Require Device Approvals",
|
||||
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||
"sshAccess": "SSH Access",
|
||||
"roleAllowSsh": "Allow SSH",
|
||||
"roleAllowSshAllow": "Allow",
|
||||
"roleAllowSshDisallow": "Disallow",
|
||||
"roleAllowSshDescription": "Allow users with this role to connect to resources via SSH. When disabled, the role cannot use SSH access.",
|
||||
"sshSudoMode": "Sudo Access",
|
||||
"sshSudoModeNone": "None",
|
||||
"sshSudoModeNoneDescription": "User cannot run commands with sudo.",
|
||||
"sshSudoModeFull": "Full Sudo",
|
||||
"sshSudoModeFullDescription": "User can run any command with sudo.",
|
||||
"sshSudoModeCommands": "Commands",
|
||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||
"sshSudo": "Allow sudo",
|
||||
"sshSudoCommands": "Sudo Commands",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.",
|
||||
"sshCreateHomeDir": "Create Home Directory",
|
||||
"sshUnixGroups": "Unix Groups",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host.",
|
||||
"retryAttempts": "Retry Attempts",
|
||||
"expectedResponseCodes": "Expected Response Codes",
|
||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||
@@ -1988,8 +2008,8 @@
|
||||
"orgAuthNoAccount": "Don't have an account?",
|
||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
|
||||
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink> or higher.",
|
||||
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> or higher to use this feature.",
|
||||
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink>.",
|
||||
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> to use this feature.",
|
||||
"subscriptionTierTier1": "Home",
|
||||
"subscriptionTierTier2": "Team",
|
||||
"subscriptionTierTier3": "Business",
|
||||
@@ -2079,7 +2099,7 @@
|
||||
"manageMachineClients": "Manage Machine Clients",
|
||||
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||
"machineClientsBannerTitle": "Servers & Automated Systems",
|
||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can be deployed as a CLI or a container.",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Container",
|
||||
@@ -2305,7 +2325,7 @@
|
||||
"logRetentionEndOfFollowingYear": "End of following year",
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature.",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
||||
"certResolver": "Certificate Resolver",
|
||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||
@@ -2503,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Access Control",
|
||||
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Location",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Choose where the SSH authentication daemon runs: on the site (Newt) or on a remote host.",
|
||||
"internalResourceAuthDaemonDescription": "The SSH authentication daemon handles SSH key signing and PAM authentication for this resource. Choose whether it runs on the site (Newt) or on a separate remote host. See <docsLink>the documentation</docsLink> for more.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Select Strategy",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Location",
|
||||
"internalResourceAuthDaemonSite": "On Site",
|
||||
"internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Remote Host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on a host that is not the site.",
|
||||
"internalResourceAuthDaemonPort": "Daemon Port (optional)",
|
||||
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
||||
"learnMore": "Learn more",
|
||||
"backToHome": "Go back to home",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Seleccionar un protocolo",
|
||||
"resourcePortNumber": "Número de puerto",
|
||||
"resourcePortNumberDescription": "El número de puerto externo a las solicitudes de proxy.",
|
||||
"back": "Atrás",
|
||||
"cancel": "Cancelar",
|
||||
"resourceConfig": "Fragmentos de configuración",
|
||||
"resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar el recurso TCP/UDP",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Se ha producido un error al eliminar la organización.",
|
||||
"orgDeleted": "Organización eliminada",
|
||||
"orgDeletedMessage": "La organización y sus datos han sido eliminados.",
|
||||
"deleteAccount": "Eliminar cuenta",
|
||||
"deleteAccountDescription": "Elimina permanentemente tu cuenta, todas las organizaciones que posees y todos los datos dentro de esas organizaciones. Esto no se puede deshacer.",
|
||||
"deleteAccountButton": "Eliminar cuenta",
|
||||
"deleteAccountConfirmTitle": "Eliminar cuenta",
|
||||
"deleteAccountConfirmMessage": "Esto borrará permanentemente tu cuenta, todas las organizaciones que posees y todos los datos dentro de esas organizaciones. Esto no se puede deshacer.",
|
||||
"deleteAccountConfirmString": "eliminar cuenta",
|
||||
"deleteAccountSuccess": "Cuenta eliminada",
|
||||
"deleteAccountSuccessMessage": "Tu cuenta ha sido eliminada.",
|
||||
"deleteAccountError": "Error al eliminar la cuenta",
|
||||
"deleteAccountPreviewAccount": "Tu cuenta",
|
||||
"deleteAccountPreviewOrgs": "Organizaciones que tienes (y todos sus datos)",
|
||||
"orgMissing": "Falta el ID de la organización",
|
||||
"orgMissingMessage": "No se puede regenerar la invitación sin el ID de la organización.",
|
||||
"accessUsersManage": "Administrar usuarios",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtrar por estado de aprobación",
|
||||
"approvalListEmpty": "No hay aprobaciones",
|
||||
"approvalState": "Estado de aprobación",
|
||||
"approvalLoadMore": "Cargar más",
|
||||
"loadingApprovals": "Cargando aprobaciones",
|
||||
"approve": "Aprobar",
|
||||
"approved": "Aprobado",
|
||||
"denied": "Denegado",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rol eliminado",
|
||||
"accessRoleRemovedDescription": "El rol se ha eliminado correctamente.",
|
||||
"accessRoleRequiredRemove": "Antes de eliminar este rol, seleccione un nuevo rol al que transferir miembros existentes.",
|
||||
"network": "Red",
|
||||
"manage": "Gestionar",
|
||||
"sitesNotFound": "Sitios no encontrados.",
|
||||
"pangolinServerAdmin": "Admin Servidor - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Configuración - Pangolin",
|
||||
"orgNameRequired": "El nombre de la organización es obligatorio",
|
||||
"orgIdRequired": "El ID de la organización es obligatorio",
|
||||
"orgIdMaxLength": "El ID de la organización debe tener como máximo 32 caracteres",
|
||||
"orgErrorCreate": "Se ha producido un error al crear el org",
|
||||
"pageNotFound": "Página no encontrada",
|
||||
"pageNotFoundDescription": "¡Vaya! La página que estás buscando no existe.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Ver registros",
|
||||
"noneSelected": "Ninguno seleccionado",
|
||||
"orgNotFound2": "No se encontraron organizaciones.",
|
||||
"searchProgress": "Buscar...",
|
||||
"searchPlaceholder": "Buscar...",
|
||||
"emptySearchOptions": "No se encontraron opciones",
|
||||
"create": "Crear",
|
||||
"orgs": "Organizaciones",
|
||||
"loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privado",
|
||||
"sidebarAccessControl": "Control de acceso",
|
||||
"sidebarLogsAndAnalytics": "Registros y análisis",
|
||||
"sidebarTeam": "Equipo",
|
||||
"sidebarUsers": "Usuarios",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Invitaciones",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Registro y análisis",
|
||||
"sidebarBluePrints": "Planos",
|
||||
"sidebarOrganization": "Organización",
|
||||
"sidebarManagement": "Gestión",
|
||||
"sidebarBillingAndLicenses": "Facturación y licencias",
|
||||
"sidebarLogsAnalytics": "Analíticas",
|
||||
"blueprints": "Planos",
|
||||
"blueprintsDescription": "Aplicar configuraciones declarativas y ver ejecuciones anteriores",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Contenido analizado (Sólo lectura)",
|
||||
"enableDockerSocket": "Habilitar Plano Docker",
|
||||
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
|
||||
"enableDockerSocketLink": "Saber más",
|
||||
"viewDockerContainers": "Ver contenedores Docker",
|
||||
"containersIn": "Contenedores en {siteName}",
|
||||
"selectContainerDescription": "Seleccione cualquier contenedor para usar como nombre de host para este objetivo. Haga clic en un puerto para usar un puerto.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Sitios",
|
||||
"billingUsers": "Usuarios",
|
||||
"billingDomains": "Dominios",
|
||||
"billingOrganizations": "Orgánico",
|
||||
"billingRemoteExitNodes": "Nodos remotos",
|
||||
"billingNoLimitConfigured": "No se ha configurado ningún límite",
|
||||
"billingEstimatedPeriod": "Período de facturación estimado",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Fallido",
|
||||
"createNewOrgDescription": "Crear una nueva organización",
|
||||
"organization": "Organización",
|
||||
"primary": "Principal",
|
||||
"port": "Puerto",
|
||||
"securityKeyManage": "Gestionar llaves de seguridad",
|
||||
"securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "El tiempo está en segundos",
|
||||
"requireDeviceApproval": "Requiere aprobaciones del dispositivo",
|
||||
"requireDeviceApprovalDescription": "Los usuarios con este rol necesitan nuevos dispositivos aprobados por un administrador antes de poder conectarse y acceder a los recursos.",
|
||||
"sshAccess": "Acceso a SSH",
|
||||
"roleAllowSsh": "Permitir SSH",
|
||||
"roleAllowSshAllow": "Permitir",
|
||||
"roleAllowSshDisallow": "Rechazar",
|
||||
"roleAllowSshDescription": "Permitir a los usuarios con este rol conectarse a recursos a través de SSH. Cuando está desactivado, el rol no puede usar acceso SSH.",
|
||||
"sshSudoMode": "Acceso Sudo",
|
||||
"sshSudoModeNone": "Ninguna",
|
||||
"sshSudoModeNoneDescription": "El usuario no puede ejecutar comandos con sudo.",
|
||||
"sshSudoModeFull": "Sudo completo",
|
||||
"sshSudoModeFullDescription": "El usuario puede ejecutar cualquier comando con sudo.",
|
||||
"sshSudoModeCommands": "Comandos",
|
||||
"sshSudoModeCommandsDescription": "El usuario sólo puede ejecutar los comandos especificados con sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos que el usuario puede ejecutar con sudo.",
|
||||
"sshCreateHomeDir": "Crear directorio principal",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix para agregar el usuario en el host de destino.",
|
||||
"retryAttempts": "Intentos de Reintento",
|
||||
"expectedResponseCodes": "Códigos de respuesta esperados",
|
||||
"expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?",
|
||||
"authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca",
|
||||
"brandingLogoURL": "URL del logotipo",
|
||||
"brandingLogoURLOrPath": "URL o ruta de Logo",
|
||||
"brandingLogoPathDescription": "Introduzca una URL o una ruta local.",
|
||||
"brandingLogoURLDescription": "Introduzca una URL de acceso público a su imagen de logotipo.",
|
||||
"brandingPrimaryColor": "Color primario",
|
||||
"brandingLogoWidth": "Ancho (px)",
|
||||
"brandingLogoHeight": "Altura (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Control de acceso",
|
||||
"editInternalResourceDialogAccessControlDescription": "Controla qué roles, usuarios y clientes de máquinas tienen acceso a este recurso cuando están conectados. Los administradores siempre tienen acceso.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "El rango de puertos debe ser \"*\" para todos los puertos, o una lista separada por comas de puertos y rangos (por ejemplo, \"80,443,8000-9000\"). Los puertos deben estar entre 1 y 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Ubicación del demonio de autenticación SSSH",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Elija dónde se ejecuta el daemon de autenticación SSH: en el sitio (Newt) o en un host remoto.",
|
||||
"internalResourceAuthDaemonDescription": "El daemon de autenticación SSSH maneja la firma de claves SSH y autenticación PAM para este recurso. Elija si se ejecuta en el sitio (Newt) o en un host remoto separado. Vea <docsLink>la documentación</docsLink> para más.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Seleccionar estrategia",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Ubicación",
|
||||
"internalResourceAuthDaemonSite": "En el sitio",
|
||||
"internalResourceAuthDaemonSiteDescription": "Auth daemon corre en el sitio (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Host remoto",
|
||||
"internalResourceAuthDaemonRemoteDescription": "El daemon Auth corre en un host que no es el sitio.",
|
||||
"internalResourceAuthDaemonPort": "Puerto de demonio (opcional)",
|
||||
"orgAuthWhatsThis": "¿Dónde puedo encontrar el ID de mi organización?",
|
||||
"learnMore": "Más información",
|
||||
"backToHome": "Volver a inicio",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Choisir un protocole",
|
||||
"resourcePortNumber": "Numéro de port",
|
||||
"resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.",
|
||||
"back": "Précédent",
|
||||
"cancel": "Abandonner",
|
||||
"resourceConfig": "Snippets de configuration",
|
||||
"resourceConfigDescription": "Copiez et collez ces extraits de configuration pour configurer la ressource TCP/UDP",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.",
|
||||
"orgDeleted": "Organisation supprimée",
|
||||
"orgDeletedMessage": "L'organisation et ses données ont été supprimées.",
|
||||
"deleteAccount": "Supprimer le compte",
|
||||
"deleteAccountDescription": "Supprimer définitivement votre compte, toutes les organisations que vous possédez et toutes les données au sein de ces organisations. Cela ne peut pas être annulé.",
|
||||
"deleteAccountButton": "Supprimer le compte",
|
||||
"deleteAccountConfirmTitle": "Supprimer le compte",
|
||||
"deleteAccountConfirmMessage": "Cela effacera définitivement votre compte, toutes les organisations que vous possédez et toutes les données au sein de ces organisations. Cela ne peut pas être annulé.",
|
||||
"deleteAccountConfirmString": "supprimer le compte",
|
||||
"deleteAccountSuccess": "Compte supprimé",
|
||||
"deleteAccountSuccessMessage": "Votre compte a été supprimé.",
|
||||
"deleteAccountError": "Échec de la suppression du compte",
|
||||
"deleteAccountPreviewAccount": "Votre Compte",
|
||||
"deleteAccountPreviewOrgs": "Organisations que vous possédez (et toutes leurs données)",
|
||||
"orgMissing": "ID d'organisation manquant",
|
||||
"orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.",
|
||||
"accessUsersManage": "Gérer les utilisateurs",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtrer par État d'Approbation",
|
||||
"approvalListEmpty": "Aucune approbation",
|
||||
"approvalState": "État d'approbation",
|
||||
"approvalLoadMore": "Charger plus",
|
||||
"loadingApprovals": "Chargement des approbations",
|
||||
"approve": "Approuver",
|
||||
"approved": "Approuvé",
|
||||
"denied": "Refusé",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rôle supprimé",
|
||||
"accessRoleRemovedDescription": "Le rôle a été supprimé avec succès.",
|
||||
"accessRoleRequiredRemove": "Avant de supprimer ce rôle, veuillez sélectionner un nouveau rôle pour transférer les membres existants.",
|
||||
"network": "Réseau",
|
||||
"manage": "Gérer",
|
||||
"sitesNotFound": "Aucun site trouvé.",
|
||||
"pangolinServerAdmin": "Admin Serveur - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Configuration - Pangolin",
|
||||
"orgNameRequired": "Le nom de l'organisation est requis",
|
||||
"orgIdRequired": "L'ID de l'organisation est requis",
|
||||
"orgIdMaxLength": "L'identifiant de l'organisation doit comporter au plus 32 caractères",
|
||||
"orgErrorCreate": "Une erreur s'est produite lors de la création de l'organisation",
|
||||
"pageNotFound": "Page non trouvée",
|
||||
"pageNotFoundDescription": "Oups! La page que vous recherchez n'existe pas.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Voir les logs",
|
||||
"noneSelected": "Aucune sélection",
|
||||
"orgNotFound2": "Aucune organisation trouvée.",
|
||||
"searchProgress": "Rechercher...",
|
||||
"searchPlaceholder": "Recherche...",
|
||||
"emptySearchOptions": "Aucune option trouvée",
|
||||
"create": "Créer",
|
||||
"orgs": "Organisations",
|
||||
"loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privé",
|
||||
"sidebarAccessControl": "Contrôle d'accès",
|
||||
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
||||
"sidebarTeam": "Equipe",
|
||||
"sidebarUsers": "Utilisateurs",
|
||||
"sidebarAdmin": "Administrateur",
|
||||
"sidebarInvitations": "Invitations",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Journaux & Analytiques",
|
||||
"sidebarBluePrints": "Configs",
|
||||
"sidebarOrganization": "Organisation",
|
||||
"sidebarManagement": "Gestion",
|
||||
"sidebarBillingAndLicenses": "Facturation & Licences",
|
||||
"sidebarLogsAnalytics": "Analyses",
|
||||
"blueprints": "Configs",
|
||||
"blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Contenu analysé (lecture seule)",
|
||||
"enableDockerSocket": "Activer la Config Docker",
|
||||
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
||||
"enableDockerSocketLink": "En savoir plus",
|
||||
"viewDockerContainers": "Voir les conteneurs Docker",
|
||||
"containersIn": "Conteneurs en {siteName}",
|
||||
"selectContainerDescription": "Sélectionnez n'importe quel conteneur à utiliser comme nom d'hôte pour cette cible. Cliquez sur un port pour utiliser un port.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Nœuds",
|
||||
"billingUsers": "Utilisateurs",
|
||||
"billingDomains": "Domaines",
|
||||
"billingOrganizations": "Organes",
|
||||
"billingRemoteExitNodes": "Nœuds distants",
|
||||
"billingNoLimitConfigured": "Aucune limite configurée",
|
||||
"billingEstimatedPeriod": "Période de facturation estimée",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Échec",
|
||||
"createNewOrgDescription": "Créer une nouvelle organisation",
|
||||
"organization": "Organisation",
|
||||
"primary": "Primaire",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Gérer les clés de sécurité",
|
||||
"securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
||||
"requireDeviceApproval": "Exiger les autorisations de l'appareil",
|
||||
"requireDeviceApprovalDescription": "Les utilisateurs ayant ce rôle ont besoin de nouveaux périphériques approuvés par un administrateur avant de pouvoir se connecter et accéder aux ressources.",
|
||||
"sshAccess": "Accès SSH",
|
||||
"roleAllowSsh": "Autoriser SSH",
|
||||
"roleAllowSshAllow": "Autoriser",
|
||||
"roleAllowSshDisallow": "Interdire",
|
||||
"roleAllowSshDescription": "Autoriser les utilisateurs avec ce rôle à se connecter aux ressources via SSH. Lorsque désactivé, le rôle ne peut pas utiliser les accès SSH.",
|
||||
"sshSudoMode": "Accès Sudo",
|
||||
"sshSudoModeNone": "Aucun",
|
||||
"sshSudoModeNoneDescription": "L'utilisateur ne peut pas exécuter de commandes avec sudo.",
|
||||
"sshSudoModeFull": "Sudo complet",
|
||||
"sshSudoModeFullDescription": "L'utilisateur peut exécuter n'importe quelle commande avec sudo.",
|
||||
"sshSudoModeCommands": "Commandes",
|
||||
"sshSudoModeCommandsDescription": "L'utilisateur ne peut exécuter que les commandes spécifiées avec sudo.",
|
||||
"sshSudo": "Autoriser sudo",
|
||||
"sshSudoCommands": "Commandes Sudo",
|
||||
"sshSudoCommandsDescription": "Liste des commandes que l'utilisateur est autorisé à exécuter avec sudo.",
|
||||
"sshCreateHomeDir": "Créer un répertoire personnel",
|
||||
"sshUnixGroups": "Groupes Unix",
|
||||
"sshUnixGroupsDescription": "Groupes Unix à ajouter à l'utilisateur sur l'hôte cible.",
|
||||
"retryAttempts": "Tentatives de réessai",
|
||||
"expectedResponseCodes": "Codes de réponse attendus",
|
||||
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?",
|
||||
"authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque",
|
||||
"brandingLogoURL": "URL du logo",
|
||||
"brandingLogoURLOrPath": "URL du logo ou du chemin d'accès",
|
||||
"brandingLogoPathDescription": "Entrez une URL ou un chemin local.",
|
||||
"brandingLogoURLDescription": "Entrez une URL accessible au public à votre image de logo.",
|
||||
"brandingPrimaryColor": "Couleur principale",
|
||||
"brandingLogoWidth": "Largeur (px)",
|
||||
"brandingLogoHeight": "Hauteur (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Contrôle d'accès",
|
||||
"editInternalResourceDialogAccessControlDescription": "Contrôlez quels rôles, utilisateurs et clients de machine ont accès à cette ressource lorsqu'ils sont connectés. Les administrateurs ont toujours accès.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "La plage de ports doit être \"*\" pour tous les ports, ou une liste de ports et de plages séparés par des virgules (par exemple, \"80,443,8000-9000\"). Les ports doivent être compris entre 1 et 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Emplacement du démon d'authentification SSH",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Choisissez où le démon d'authentification SSH s'exécute : sur le site (Newt) ou sur un hôte distant.",
|
||||
"internalResourceAuthDaemonDescription": "Le démon d'authentification SSH gère la signature des clés SSH et l'authentification PAM pour cette ressource. Choisissez s'il fonctionne sur le site (Newt) ou sur un hôte distant séparé. Voir <docsLink>la documentation</docsLink> pour plus d'informations.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Choisir une stratégie",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Localisation",
|
||||
"internalResourceAuthDaemonSite": "Sur le site",
|
||||
"internalResourceAuthDaemonSiteDescription": "Le démon Auth fonctionne sur le site (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Hôte distant",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Le démon Auth fonctionne sur un hôte qui n'est pas le site.",
|
||||
"internalResourceAuthDaemonPort": "Port du démon (optionnel)",
|
||||
"orgAuthWhatsThis": "Où puis-je trouver mon identifiant d'organisation ?",
|
||||
"learnMore": "En savoir plus",
|
||||
"backToHome": "Retour à l'accueil",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Seleziona un protocollo",
|
||||
"resourcePortNumber": "Numero Porta",
|
||||
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"resourceConfig": "Snippet Di Configurazione",
|
||||
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione dell'organizzazione.",
|
||||
"orgDeleted": "Organizzazione eliminata",
|
||||
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
||||
"deleteAccount": "Elimina Account",
|
||||
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
||||
"deleteAccountButton": "Elimina Account",
|
||||
"deleteAccountConfirmTitle": "Elimina Account",
|
||||
"deleteAccountConfirmMessage": "Questo cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
||||
"deleteAccountConfirmString": "elimina account",
|
||||
"deleteAccountSuccess": "Account Eliminato",
|
||||
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
||||
"deleteAccountError": "Impossibile eliminare l'account",
|
||||
"deleteAccountPreviewAccount": "Il Tuo Account",
|
||||
"deleteAccountPreviewOrgs": "Organizzazioni che possiedi (e tutti i loro dati)",
|
||||
"orgMissing": "ID Organizzazione Mancante",
|
||||
"orgMissingMessage": "Impossibile rigenerare l'invito senza un ID organizzazione.",
|
||||
"accessUsersManage": "Gestisci Utenti",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtra Per Stato Di Approvazione",
|
||||
"approvalListEmpty": "Nessuna approvazione",
|
||||
"approvalState": "Stato Di Approvazione",
|
||||
"approvalLoadMore": "Carica altro",
|
||||
"loadingApprovals": "Caricamento Approvazioni",
|
||||
"approve": "Approva",
|
||||
"approved": "Approvato",
|
||||
"denied": "Negato",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Ruolo rimosso",
|
||||
"accessRoleRemovedDescription": "Il ruolo è stato rimosso con successo.",
|
||||
"accessRoleRequiredRemove": "Prima di eliminare questo ruolo, seleziona un nuovo ruolo a cui trasferire i membri esistenti.",
|
||||
"network": "Rete",
|
||||
"manage": "Gestisci",
|
||||
"sitesNotFound": "Nessun sito trovato.",
|
||||
"pangolinServerAdmin": "Server Admin - Pangolina",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Configurazione - Pangolin",
|
||||
"orgNameRequired": "Il nome dell'organizzazione è obbligatorio",
|
||||
"orgIdRequired": "L'ID dell'organizzazione è obbligatorio",
|
||||
"orgIdMaxLength": "L'ID dell'organizzazione deve contenere al massimo 32 caratteri",
|
||||
"orgErrorCreate": "Si è verificato un errore durante la creazione dell'organizzazione",
|
||||
"pageNotFound": "Pagina Non Trovata",
|
||||
"pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Visualizza Log",
|
||||
"noneSelected": "Nessuna selezione",
|
||||
"orgNotFound2": "Nessuna organizzazione trovata.",
|
||||
"searchProgress": "Ricerca...",
|
||||
"searchPlaceholder": "Cerca...",
|
||||
"emptySearchOptions": "Nessuna opzione trovata",
|
||||
"create": "Crea",
|
||||
"orgs": "Organizzazioni",
|
||||
"loginError": "Si è verificato un errore imprevisto. Riprova.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privato",
|
||||
"sidebarAccessControl": "Controllo Accesso",
|
||||
"sidebarLogsAndAnalytics": "Registri E Analisi",
|
||||
"sidebarTeam": "Squadra",
|
||||
"sidebarUsers": "Utenti",
|
||||
"sidebarAdmin": "Amministratore",
|
||||
"sidebarInvitations": "Inviti",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Progetti",
|
||||
"sidebarOrganization": "Organizzazione",
|
||||
"sidebarManagement": "Gestione",
|
||||
"sidebarBillingAndLicenses": "Fatturazione E Licenze",
|
||||
"sidebarLogsAnalytics": "Analisi",
|
||||
"blueprints": "Progetti",
|
||||
"blueprintsDescription": "Applica le configurazioni dichiarative e visualizza le partite precedenti",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Sommario Analizzato (Solo Lettura)",
|
||||
"enableDockerSocket": "Abilita Progetto Docker",
|
||||
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
|
||||
"enableDockerSocketLink": "Scopri di più",
|
||||
"viewDockerContainers": "Visualizza Contenitori Docker",
|
||||
"containersIn": "Contenitori in {siteName}",
|
||||
"selectContainerDescription": "Seleziona qualsiasi contenitore da usare come hostname per questo obiettivo. Fai clic su una porta per usare una porta.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Siti",
|
||||
"billingUsers": "Utenti",
|
||||
"billingDomains": "Domini",
|
||||
"billingOrganizations": "Organi",
|
||||
"billingRemoteExitNodes": "Nodi Remoti",
|
||||
"billingNoLimitConfigured": "Nessun limite configurato",
|
||||
"billingEstimatedPeriod": "Periodo di Fatturazione Stimato",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Fallito",
|
||||
"createNewOrgDescription": "Crea una nuova organizzazione",
|
||||
"organization": "Organizzazione",
|
||||
"primary": "Principale",
|
||||
"port": "Porta",
|
||||
"securityKeyManage": "Gestisci chiavi di sicurezza",
|
||||
"securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Il tempo è in secondi",
|
||||
"requireDeviceApproval": "Richiede Approvazioni Dispositivo",
|
||||
"requireDeviceApprovalDescription": "Gli utenti con questo ruolo hanno bisogno di nuovi dispositivi approvati da un amministratore prima di poter connettersi e accedere alle risorse.",
|
||||
"sshAccess": "Accesso SSH",
|
||||
"roleAllowSsh": "Consenti SSH",
|
||||
"roleAllowSshAllow": "Consenti",
|
||||
"roleAllowSshDisallow": "Non Consentire",
|
||||
"roleAllowSshDescription": "Consenti agli utenti con questo ruolo di connettersi alle risorse tramite SSH. Quando disabilitato, il ruolo non può utilizzare l'accesso SSH.",
|
||||
"sshSudoMode": "Accesso Sudo",
|
||||
"sshSudoModeNone": "Nessuno",
|
||||
"sshSudoModeNoneDescription": "L'utente non può eseguire comandi con sudo.",
|
||||
"sshSudoModeFull": "Sudo Completo",
|
||||
"sshSudoModeFullDescription": "L'utente può eseguire qualsiasi comando con sudo.",
|
||||
"sshSudoModeCommands": "Comandi",
|
||||
"sshSudoModeCommandsDescription": "L'utente può eseguire solo i comandi specificati con sudo.",
|
||||
"sshSudo": "Consenti sudo",
|
||||
"sshSudoCommands": "Comandi Sudo",
|
||||
"sshSudoCommandsDescription": "Elenco di comandi che l'utente può eseguire con sudo.",
|
||||
"sshCreateHomeDir": "Crea Cartella Home",
|
||||
"sshUnixGroups": "Gruppi Unix",
|
||||
"sshUnixGroupsDescription": "Gruppi Unix su cui aggiungere l'utente sull'host di destinazione.",
|
||||
"retryAttempts": "Tentativi di Riprova",
|
||||
"expectedResponseCodes": "Codici di Risposta Attesi",
|
||||
"expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?",
|
||||
"authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding",
|
||||
"brandingLogoURL": "URL Logo",
|
||||
"brandingLogoURLOrPath": "URL o percorso del logo",
|
||||
"brandingLogoPathDescription": "Inserisci un URL o un percorso locale.",
|
||||
"brandingLogoURLDescription": "Inserisci un URL accessibile al pubblico per la tua immagine del logo.",
|
||||
"brandingPrimaryColor": "Colore Primario",
|
||||
"brandingLogoWidth": "Larghezza (px)",
|
||||
"brandingLogoHeight": "Altezza (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Controllo Accesso",
|
||||
"editInternalResourceDialogAccessControlDescription": "Controlla quali ruoli, utenti e client macchina hanno accesso a questa risorsa quando connessi. Gli amministratori hanno sempre accesso.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Il range delle porte deve essere \"*\" per tutte le porte, o un elenco di porte e intervalli separato da virgole (ad es. \"80,443,8000-9000\"). Le porte devono essere tra 1 e 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Posizione Demone Autenticazione SSH",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Scegli dove funziona il demone di autenticazione SSH: sul sito (Newt) o su un host remoto.",
|
||||
"internalResourceAuthDaemonDescription": "Il demone di autenticazione SSH gestisce la firma della chiave SSH e l'autenticazione PAM per questa risorsa. Scegli se viene eseguito sul sito (Newt) o su un host remoto separato. Vedi <docsLink>la documentazione</docsLink> per ulteriori informazioni.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Seleziona Strategia",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Posizione",
|
||||
"internalResourceAuthDaemonSite": "Sul Sito",
|
||||
"internalResourceAuthDaemonSiteDescription": "Il demone Auth viene eseguito sul sito (Nuovo).",
|
||||
"internalResourceAuthDaemonRemote": "Host Remoto",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Il demone di autenticazione viene eseguito su un host che non è il sito.",
|
||||
"internalResourceAuthDaemonPort": "Porta Demone (facoltativa)",
|
||||
"orgAuthWhatsThis": "Dove posso trovare l'ID della mia organizzazione?",
|
||||
"learnMore": "Scopri di più",
|
||||
"backToHome": "Torna alla home",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "프로토콜 선택",
|
||||
"resourcePortNumber": "포트 번호",
|
||||
"resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.",
|
||||
"back": "뒤로",
|
||||
"cancel": "취소",
|
||||
"resourceConfig": "구성 스니펫",
|
||||
"resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣습니다.",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "조직을 삭제하는 중 오류가 발생했습니다.",
|
||||
"orgDeleted": "조직이 삭제되었습니다.",
|
||||
"orgDeletedMessage": "조직과 그 데이터가 삭제되었습니다.",
|
||||
"deleteAccount": "계정 삭제",
|
||||
"deleteAccountDescription": "계정, 소유한 모든 조직 및 조직 내의 모든 데이터를 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
"deleteAccountButton": "계정 삭제",
|
||||
"deleteAccountConfirmTitle": "계정 삭제",
|
||||
"deleteAccountConfirmMessage": "이 작업은 귀하의 계정, 소유한 모든 조직 및 조직 내 모든 데이터를 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
"deleteAccountConfirmString": "계정 삭제",
|
||||
"deleteAccountSuccess": "계정 삭제됨",
|
||||
"deleteAccountSuccessMessage": "계정이 삭제되었습니다.",
|
||||
"deleteAccountError": "계정 삭제 실패",
|
||||
"deleteAccountPreviewAccount": "귀하의 계정",
|
||||
"deleteAccountPreviewOrgs": "귀하가 소유한 조직(포함된 모든 데이터)",
|
||||
"orgMissing": "조직 ID가 누락되었습니다",
|
||||
"orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.",
|
||||
"accessUsersManage": "사용자 관리",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "승인 상태로 필터링",
|
||||
"approvalListEmpty": "승인이 없습니다.",
|
||||
"approvalState": "승인 상태",
|
||||
"approvalLoadMore": "더 불러오기",
|
||||
"loadingApprovals": "승인 불러오는 중",
|
||||
"approve": "승인",
|
||||
"approved": "승인됨",
|
||||
"denied": "거부됨",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "역할이 제거되었습니다",
|
||||
"accessRoleRemovedDescription": "역할이 성공적으로 제거되었습니다.",
|
||||
"accessRoleRequiredRemove": "이 역할을 삭제하기 전에 기존 구성원을 전송할 새 역할을 선택하세요.",
|
||||
"network": "네트워크",
|
||||
"manage": "관리",
|
||||
"sitesNotFound": "사이트를 찾을 수 없습니다.",
|
||||
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "설정 - 판골린",
|
||||
"orgNameRequired": "조직 이름은 필수입니다.",
|
||||
"orgIdRequired": "조직 ID가 필요합니다",
|
||||
"orgIdMaxLength": "조직 ID는 최대 32자 이내여야 합니다",
|
||||
"orgErrorCreate": "조직 생성 중 오류가 발생했습니다.",
|
||||
"pageNotFound": "페이지를 찾을 수 없습니다",
|
||||
"pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "로그 보기",
|
||||
"noneSelected": "선택된 항목 없음",
|
||||
"orgNotFound2": "조직이 없습니다.",
|
||||
"searchProgress": "검색...",
|
||||
"searchPlaceholder": "검색...",
|
||||
"emptySearchOptions": "옵션이 없습니다",
|
||||
"create": "생성",
|
||||
"orgs": "조직",
|
||||
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "비공개",
|
||||
"sidebarAccessControl": "액세스 제어",
|
||||
"sidebarLogsAndAnalytics": "로그 및 분석",
|
||||
"sidebarTeam": "팀",
|
||||
"sidebarUsers": "사용자",
|
||||
"sidebarAdmin": "관리자",
|
||||
"sidebarInvitations": "초대",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "로그 & 통계",
|
||||
"sidebarBluePrints": "청사진",
|
||||
"sidebarOrganization": "조직",
|
||||
"sidebarManagement": "관리",
|
||||
"sidebarBillingAndLicenses": "결제 및 라이선스",
|
||||
"sidebarLogsAnalytics": "분석",
|
||||
"blueprints": "청사진",
|
||||
"blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "구문 분석된 콘텐츠 (읽기 전용)",
|
||||
"enableDockerSocket": "Docker 청사진 활성화",
|
||||
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
"enableDockerSocketLink": "자세히 알아보기",
|
||||
"viewDockerContainers": "도커 컨테이너 보기",
|
||||
"containersIn": "{siteName}의 컨테이너",
|
||||
"selectContainerDescription": "이 대상을 위한 호스트 이름으로 사용할 컨테이너를 선택하세요. 포트를 사용하려면 포트를 클릭하세요.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "사이트",
|
||||
"billingUsers": "사용자",
|
||||
"billingDomains": "도메인",
|
||||
"billingOrganizations": "조직",
|
||||
"billingRemoteExitNodes": "원격 노드",
|
||||
"billingNoLimitConfigured": "구성된 한도가 없습니다.",
|
||||
"billingEstimatedPeriod": "예상 청구 기간",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "실패",
|
||||
"createNewOrgDescription": "새 조직 생성",
|
||||
"organization": "조직",
|
||||
"primary": "기본",
|
||||
"port": "포트",
|
||||
"securityKeyManage": "보안 키 관리",
|
||||
"securityKeyDescription": "비밀번호 없는 인증을 위해 보안 키를 추가하거나 제거합니다.",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "시간은 초 단위입니다",
|
||||
"requireDeviceApproval": "장치 승인 요구",
|
||||
"requireDeviceApprovalDescription": "이 역할을 가진 사용자는 장치가 연결되기 전에 관리자의 승인이 필요합니다.",
|
||||
"sshAccess": "SSH 접속",
|
||||
"roleAllowSsh": "SSH 허용",
|
||||
"roleAllowSshAllow": "허용",
|
||||
"roleAllowSshDisallow": "허용 안 함",
|
||||
"roleAllowSshDescription": "이 역할을 가진 사용자가 SSH를 통해 리소스에 연결할 수 있도록 허용합니다. 비활성화되면 역할은 SSH 접속을 사용할 수 없습니다.",
|
||||
"sshSudoMode": "Sudo 접속",
|
||||
"sshSudoModeNone": "없음",
|
||||
"sshSudoModeNoneDescription": "사용자는 sudo로 명령을 실행할 수 없습니다.",
|
||||
"sshSudoModeFull": "전체 Sudo",
|
||||
"sshSudoModeFullDescription": "사용자는 모든 명령을 sudo로 실행할 수 있습니다.",
|
||||
"sshSudoModeCommands": "명령",
|
||||
"sshSudoModeCommandsDescription": "사용자는 sudo로 지정된 명령만 실행할 수 있습니다.",
|
||||
"sshSudo": "Sudo 허용",
|
||||
"sshSudoCommands": "Sudo 명령",
|
||||
"sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있도록 허용된 명령 목록입니다.",
|
||||
"sshCreateHomeDir": "홈 디렉터리 생성",
|
||||
"sshUnixGroups": "유닉스 그룹",
|
||||
"sshUnixGroupsDescription": "대상 호스트에서 사용자를 추가할 유닉스 그룹입니다.",
|
||||
"retryAttempts": "재시도 횟수",
|
||||
"expectedResponseCodes": "예상 응답 코드",
|
||||
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
|
||||
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
|
||||
"brandingLogoURL": "로고 URL",
|
||||
"brandingLogoURLOrPath": "로고 URL 또는 경로",
|
||||
"brandingLogoPathDescription": "URL 또는 로컬 경로를 입력하세요.",
|
||||
"brandingLogoURLDescription": "로고 이미지에 대한 공용 URL을 입력하십시오.",
|
||||
"brandingPrimaryColor": "기본 색상",
|
||||
"brandingLogoWidth": "너비(px)",
|
||||
"brandingLogoHeight": "높이(px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "액세스 제어",
|
||||
"editInternalResourceDialogAccessControlDescription": "연결 시 이 리소스에 대한 액세스 권한을 가지는 역할, 사용자, 그리고 머신 클라이언트를 제어합니다. 관리자는 항상 접근할 수 있습니다.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "모든 포트에 대해서는 \"*\"로, 아니면 쉼표로 구분된 포트 및 범위 목록(예: \"80,443,8000-9000\")을 설정해야 합니다. 포트는 1에서 65535 사이여야 합니다.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH 인증 데몬 위치",
|
||||
"internalResourceAuthDaemonStrategyDescription": "SSH 인증 데몬이 작동하는 위치를 선택하세요: 사이트(Newt)에서 또는 원격 호스트에서.",
|
||||
"internalResourceAuthDaemonDescription": "SSH 인증 데몬은 이 리소스를 위한 SSH 키 서명과 PAM 인증을 처리합니다. 사이트(Newt)에서 나 별도의 원격 호스트에서 실행할 것인지를 선택하세요. 자세한 내용은 <docsLink>문서</docsLink>를 참조하세요.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "전략 선택",
|
||||
"internalResourceAuthDaemonStrategyLabel": "위치",
|
||||
"internalResourceAuthDaemonSite": "사이트에서 인증 데몬이 실행됩니다(Newt).",
|
||||
"internalResourceAuthDaemonSiteDescription": "인증 데몬이 사이트(Newt)에서 실행됩니다.",
|
||||
"internalResourceAuthDaemonRemote": "원격 호스트",
|
||||
"internalResourceAuthDaemonRemoteDescription": "인증 데몬이 사이트가 아닌 다른 호스트에서 실행됩니다.",
|
||||
"internalResourceAuthDaemonPort": "데몬 포트 (선택 사항)",
|
||||
"orgAuthWhatsThis": "조직 ID를 어디에서 찾을 수 있습니까?",
|
||||
"learnMore": "자세히 알아보기",
|
||||
"backToHome": "홈으로 돌아가기",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Velg en protokoll",
|
||||
"resourcePortNumber": "Portnummer",
|
||||
"resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.",
|
||||
"back": "Tilbake",
|
||||
"cancel": "Avbryt",
|
||||
"resourceConfig": "Konfigurasjonsutdrag",
|
||||
"resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.",
|
||||
"orgDeleted": "Organisasjon slettet",
|
||||
"orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.",
|
||||
"deleteAccount": "Slett konto",
|
||||
"deleteAccountDescription": "Slett kontoen din permanent, alle organisasjoner du eier, og alle data i disse organisasjonene. Dette kan ikke angres.",
|
||||
"deleteAccountButton": "Slett konto",
|
||||
"deleteAccountConfirmTitle": "Slett konto",
|
||||
"deleteAccountConfirmMessage": "Dette vil slette kontoen din, alle organisasjoner du eier og alle data i disse organisasjonene. Dette kan ikke gjøres om.",
|
||||
"deleteAccountConfirmString": "Slett konto",
|
||||
"deleteAccountSuccess": "Kontoen er slettet",
|
||||
"deleteAccountSuccessMessage": "Kontoen din er slettet.",
|
||||
"deleteAccountError": "Kunne ikke slette konto",
|
||||
"deleteAccountPreviewAccount": "Din konto",
|
||||
"deleteAccountPreviewOrgs": "Organisasjoner du eier (og alle deres data)",
|
||||
"orgMissing": "Organisasjons-ID Mangler",
|
||||
"orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.",
|
||||
"accessUsersManage": "Administrer brukere",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtrer etter godkjenningsstatus",
|
||||
"approvalListEmpty": "Ingen godkjenninger",
|
||||
"approvalState": "Godkjennings tilstand",
|
||||
"approvalLoadMore": "Last mer",
|
||||
"loadingApprovals": "Laster inn godkjenninger",
|
||||
"approve": "Godkjenn",
|
||||
"approved": "Godkjent",
|
||||
"denied": "Avvist",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rolle fjernet",
|
||||
"accessRoleRemovedDescription": "Rollen er vellykket fjernet.",
|
||||
"accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.",
|
||||
"network": "Nettverk",
|
||||
"manage": "Administrer",
|
||||
"sitesNotFound": "Ingen områder funnet.",
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Oppsett - Pangolin",
|
||||
"orgNameRequired": "Organisasjonsnavn er påkrevd",
|
||||
"orgIdRequired": "Organisasjons-ID er påkrevd",
|
||||
"orgIdMaxLength": "Organisasjons-ID må maksimalt være 32 tegn",
|
||||
"orgErrorCreate": "En feil oppstod under oppretting av organisasjon",
|
||||
"pageNotFound": "Siden ble ikke funnet",
|
||||
"pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Vis logger",
|
||||
"noneSelected": "Ingen valgt",
|
||||
"orgNotFound2": "Ingen organisasjoner funnet.",
|
||||
"searchProgress": "Søker...",
|
||||
"searchPlaceholder": "Søk...",
|
||||
"emptySearchOptions": "Ingen valg funnet",
|
||||
"create": "Opprett",
|
||||
"orgs": "Organisasjoner",
|
||||
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privat",
|
||||
"sidebarAccessControl": "Tilgangskontroll",
|
||||
"sidebarLogsAndAnalytics": "Logger og analyser",
|
||||
"sidebarTeam": "Lag",
|
||||
"sidebarUsers": "Brukere",
|
||||
"sidebarAdmin": "Administrator",
|
||||
"sidebarInvitations": "Invitasjoner",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Logg og analyser",
|
||||
"sidebarBluePrints": "Tegninger",
|
||||
"sidebarOrganization": "Organisasjon",
|
||||
"sidebarManagement": "Administrasjon",
|
||||
"sidebarBillingAndLicenses": "Fakturering & lisenser",
|
||||
"sidebarLogsAnalytics": "Analyser",
|
||||
"blueprints": "Tegninger",
|
||||
"blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Parastinnhold (kun lese)",
|
||||
"enableDockerSocket": "Aktiver Docker blåkopi",
|
||||
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
||||
"enableDockerSocketLink": "Lær mer",
|
||||
"viewDockerContainers": "Vis Docker-containere",
|
||||
"containersIn": "Containere i {siteName}",
|
||||
"selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Områder",
|
||||
"billingUsers": "Brukere",
|
||||
"billingDomains": "Domener",
|
||||
"billingOrganizations": "Orger",
|
||||
"billingRemoteExitNodes": "Eksterne Noder",
|
||||
"billingNoLimitConfigured": "Ingen grense konfigurert",
|
||||
"billingEstimatedPeriod": "Estimert faktureringsperiode",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Mislyktes",
|
||||
"createNewOrgDescription": "Opprett en ny organisasjon",
|
||||
"organization": "Organisasjon",
|
||||
"primary": "Primær",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Administrer sikkerhetsnøkler",
|
||||
"securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Tid er i sekunder",
|
||||
"requireDeviceApproval": "Krev enhetsgodkjenning",
|
||||
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
|
||||
"sshAccess": "SSH tilgang",
|
||||
"roleAllowSsh": "Tillat SSH",
|
||||
"roleAllowSshAllow": "Tillat",
|
||||
"roleAllowSshDisallow": "Forby",
|
||||
"roleAllowSshDescription": "Tillat brukere med denne rollen å koble til ressurser via SSH. Når deaktivert får rollen ikke tilgang til SSH.",
|
||||
"sshSudoMode": "Sudo tilgang",
|
||||
"sshSudoModeNone": "Ingen",
|
||||
"sshSudoModeNoneDescription": "Brukeren kan ikke kjøre kommandoer med sudo.",
|
||||
"sshSudoModeFull": "Full Sudo",
|
||||
"sshSudoModeFullDescription": "Brukeren kan kjøre hvilken som helst kommando med sudo.",
|
||||
"sshSudoModeCommands": "Kommandoer",
|
||||
"sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.",
|
||||
"sshSudo": "Tillat sudo",
|
||||
"sshSudoCommands": "Sudo kommandoer",
|
||||
"sshSudoCommandsDescription": "Liste av kommandoer brukeren har lov til å kjøre med sudo.",
|
||||
"sshCreateHomeDir": "Opprett hjemmappe",
|
||||
"sshUnixGroups": "Unix grupper",
|
||||
"sshUnixGroupsDescription": "Unix grupper for å legge til brukeren til målverten.",
|
||||
"retryAttempts": "Forsøk på nytt",
|
||||
"expectedResponseCodes": "Forventede svarkoder",
|
||||
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
|
||||
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "Logoen URL eller sti",
|
||||
"brandingLogoPathDescription": "Skriv inn en URL eller en lokal bane.",
|
||||
"brandingLogoURLDescription": "Skriv inn en offentlig tilgjengelig nettadresse til din logobilde.",
|
||||
"brandingPrimaryColor": "Primærfarge",
|
||||
"brandingLogoWidth": "Bredde (px)",
|
||||
"brandingLogoHeight": "Høyde (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Tilgangskontroll",
|
||||
"editInternalResourceDialogAccessControlDescription": "Kontroller hvilke roller, brukere og maskinklienter som har tilgang til denne ressursen når den er koblet til. Administratorer har alltid tilgang.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Portsjiktet må være \"*\" for alle porter, eller en kommaseparert liste med porter og sjikt (f.eks. \"80,443,8000-9000\"). Porter må være mellom 1 og 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Sted",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Velg hvor SSH-autentisering daemon kjører: på nettstedet (Newt) eller på en ekstern vert.",
|
||||
"internalResourceAuthDaemonDescription": "SSH-godkjenning daemon håndterer SSH-nøkkel signering og PAM autentisering for denne ressursen. Velg om den kjører på nettstedet (Newt) eller på en separat ekstern vert. Se <docsLink>dokumentasjonen</docsLink> for mer.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Velg strategi",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Sted",
|
||||
"internalResourceAuthDaemonSite": "På nettsted",
|
||||
"internalResourceAuthDaemonSiteDescription": "Autentiser daemon kjører på nettstedet (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Ekstern vert",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Autentiser daemon kjører på en vert som ikke er nettstedet.",
|
||||
"internalResourceAuthDaemonPort": "Daemon Port (valgfritt)",
|
||||
"orgAuthWhatsThis": "Hvor kan jeg finne min organisasjons-ID?",
|
||||
"learnMore": "Lær mer",
|
||||
"backToHome": "Gå tilbake til start",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Selecteer een protocol",
|
||||
"resourcePortNumber": "Nummer van poort",
|
||||
"resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.",
|
||||
"back": "Achterzijde",
|
||||
"cancel": "Annuleren",
|
||||
"resourceConfig": "Configuratie tekstbouwstenen",
|
||||
"resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.",
|
||||
"orgDeleted": "Organisatie verwijderd",
|
||||
"orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.",
|
||||
"deleteAccount": "Verwijder account",
|
||||
"deleteAccountDescription": "Verwijdert permanent uw account, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
|
||||
"deleteAccountButton": "Verwijder account",
|
||||
"deleteAccountConfirmTitle": "Verwijder account",
|
||||
"deleteAccountConfirmMessage": "Dit zal uw account permanent wissen, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
|
||||
"deleteAccountConfirmString": "verwijder account",
|
||||
"deleteAccountSuccess": "Account verwijderd",
|
||||
"deleteAccountSuccessMessage": "Uw account is verwijderd.",
|
||||
"deleteAccountError": "Kan account niet verwijderen",
|
||||
"deleteAccountPreviewAccount": "Uw account",
|
||||
"deleteAccountPreviewOrgs": "Organisaties die je bezit (en al hun gegevens)",
|
||||
"orgMissing": "Organisatie-ID ontbreekt",
|
||||
"orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.",
|
||||
"accessUsersManage": "Gebruikers beheren",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filter op goedkeuringsstatus",
|
||||
"approvalListEmpty": "Geen goedkeuringen",
|
||||
"approvalState": "Goedkeuring status",
|
||||
"approvalLoadMore": "Meer laden",
|
||||
"loadingApprovals": "Goedkeuringen laden",
|
||||
"approve": "Goedkeuren",
|
||||
"approved": "Goedgekeurd",
|
||||
"denied": "Geweigerd",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rol verwijderd",
|
||||
"accessRoleRemovedDescription": "De rol is succesvol verwijderd.",
|
||||
"accessRoleRequiredRemove": "Voordat u deze rol verwijdert, selecteer een nieuwe rol om bestaande leden aan te dragen.",
|
||||
"network": "Netwerk",
|
||||
"manage": "Beheren",
|
||||
"sitesNotFound": "Geen sites gevonden.",
|
||||
"pangolinServerAdmin": "Serverbeheer - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Instellen - Pangolin",
|
||||
"orgNameRequired": "Organisatienaam is vereist",
|
||||
"orgIdRequired": "Organisatie-ID is vereist",
|
||||
"orgIdMaxLength": "Organisatie-ID mag maximaal 32 tekens lang zijn",
|
||||
"orgErrorCreate": "Fout opgetreden tijdens het aanmaken org",
|
||||
"pageNotFound": "Pagina niet gevonden",
|
||||
"pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Logboeken bekijken",
|
||||
"noneSelected": "Niet geselecteerd",
|
||||
"orgNotFound2": "Geen organisaties gevonden.",
|
||||
"searchProgress": "Zoeken...",
|
||||
"searchPlaceholder": "Zoeken...",
|
||||
"emptySearchOptions": "Geen opties gevonden",
|
||||
"create": "Aanmaken",
|
||||
"orgs": "Organisaties",
|
||||
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privé",
|
||||
"sidebarAccessControl": "Toegangs controle",
|
||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Gebruikers",
|
||||
"sidebarAdmin": "Beheerder",
|
||||
"sidebarInvitations": "Uitnodigingen",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Blauwdrukken",
|
||||
"sidebarOrganization": "Organisatie",
|
||||
"sidebarManagement": "Beheer",
|
||||
"sidebarBillingAndLicenses": "Facturatie & Licenties",
|
||||
"sidebarLogsAnalytics": "Analyses",
|
||||
"blueprints": "Blauwdrukken",
|
||||
"blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Geparseerde inhoud (alleen lezen)",
|
||||
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
||||
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
||||
"enableDockerSocketLink": "Meer informatie",
|
||||
"viewDockerContainers": "Bekijk Docker containers",
|
||||
"containersIn": "Containers in {siteName}",
|
||||
"selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Sites",
|
||||
"billingUsers": "Gebruikers",
|
||||
"billingDomains": "Domeinen",
|
||||
"billingOrganizations": "Ordenen",
|
||||
"billingRemoteExitNodes": "Externe knooppunten",
|
||||
"billingNoLimitConfigured": "Geen limiet ingesteld",
|
||||
"billingEstimatedPeriod": "Geschatte Facturatie Periode",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Mislukt",
|
||||
"createNewOrgDescription": "Maak een nieuwe organisatie",
|
||||
"organization": "Organisatie",
|
||||
"primary": "Primair",
|
||||
"port": "Poort",
|
||||
"securityKeyManage": "Beveiligingssleutels beheren",
|
||||
"securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Tijd is in seconden",
|
||||
"requireDeviceApproval": "Vereist goedkeuring van apparaat",
|
||||
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
|
||||
"sshAccess": "SSH toegang",
|
||||
"roleAllowSsh": "SSH toestaan",
|
||||
"roleAllowSshAllow": "Toestaan",
|
||||
"roleAllowSshDisallow": "Weigeren",
|
||||
"roleAllowSshDescription": "Sta gebruikers met deze rol toe om verbinding te maken met bronnen via SSH. Indien uitgeschakeld kan de rol geen gebruik maken van SSH toegang.",
|
||||
"sshSudoMode": "Sudo toegang",
|
||||
"sshSudoModeNone": "geen",
|
||||
"sshSudoModeNoneDescription": "Gebruiker kan geen commando's uitvoeren met sudo.",
|
||||
"sshSudoModeFull": "Volledige Sudo",
|
||||
"sshSudoModeFullDescription": "Gebruiker kan elk commando uitvoeren met een sudo.",
|
||||
"sshSudoModeCommands": "Opdrachten",
|
||||
"sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.",
|
||||
"sshSudo": "sudo toestaan",
|
||||
"sshSudoCommands": "Sudo Commando's",
|
||||
"sshSudoCommandsDescription": "Lijst van commando's die de gebruiker mag uitvoeren met een sudo.",
|
||||
"sshCreateHomeDir": "Maak Home Directory",
|
||||
"sshUnixGroups": "Unix groepen",
|
||||
"sshUnixGroupsDescription": "Unix groepen om de gebruiker toe te voegen aan de doel host.",
|
||||
"retryAttempts": "Herhaal Pogingen",
|
||||
"expectedResponseCodes": "Verwachte Reactiecodes",
|
||||
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
|
||||
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
|
||||
"brandingLogoURL": "Het logo-URL",
|
||||
"brandingLogoURLOrPath": "Logo URL of pad",
|
||||
"brandingLogoPathDescription": "Voer een URL of een lokaal pad in.",
|
||||
"brandingLogoURLDescription": "Voer een openbaar toegankelijke URL in voor uw logo afbeelding.",
|
||||
"brandingPrimaryColor": "Primaire kleur",
|
||||
"brandingLogoWidth": "Breedte (px)",
|
||||
"brandingLogoHeight": "Hoogte (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Toegangs controle",
|
||||
"editInternalResourceDialogAccessControlDescription": "Beheer welke rollen, gebruikers en machineclients toegang hebben tot deze bron wanneer ze zijn verbonden. Beheerders hebben altijd toegang.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Poortbereik moet \"*\" zijn voor alle poorten, of een komma-gescheiden lijst van poorten en bereiken (bijv. \"80,443,8000-9000\"). Poorten moeten tussen 1 en 65535 zijn.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon locatie",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Kies waar de SSH authenticatie daemon wordt uitgevoerd: op de website (Newt) of op een externe host.",
|
||||
"internalResourceAuthDaemonDescription": "De SSH authenticatie daemon zorgt voor SSH sleutelondertekening en PAM authenticatie voor deze resource. Kies of het wordt uitgevoerd op de website (Nieuw) of op een afzonderlijke externe host. Zie <docsLink>de documentatie</docsLink> voor meer.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Selecteer strategie",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Locatie",
|
||||
"internalResourceAuthDaemonSite": "In de site",
|
||||
"internalResourceAuthDaemonSiteDescription": "Auth daemon draait op de site (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Externe host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Authenticatiedaemon draait op een host die niet de site is.",
|
||||
"internalResourceAuthDaemonPort": "Daemon poort (optioneel)",
|
||||
"orgAuthWhatsThis": "Waar kan ik mijn organisatie-ID vinden?",
|
||||
"learnMore": "Meer informatie",
|
||||
"backToHome": "Ga terug naar startpagina",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Wybierz protokół",
|
||||
"resourcePortNumber": "Numer portu",
|
||||
"resourcePortNumberDescription": "Numer portu zewnętrznego do żądań proxy.",
|
||||
"back": "Powrót",
|
||||
"cancel": "Anuluj",
|
||||
"resourceConfig": "Snippety konfiguracji",
|
||||
"resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować zasób TCP/UDP",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Wystąpił błąd podczas usuwania organizacji.",
|
||||
"orgDeleted": "Organizacja usunięta",
|
||||
"orgDeletedMessage": "Organizacja i jej dane zostały usunięte.",
|
||||
"deleteAccount": "Usuń konto",
|
||||
"deleteAccountDescription": "Trwale usuń swoje konto, wszystkie organizacje, które posiadasz, oraz wszystkie dane w ramach tych organizacji. Tej operacji nie można cofnąć.",
|
||||
"deleteAccountButton": "Usuń konto",
|
||||
"deleteAccountConfirmTitle": "Usuń konto",
|
||||
"deleteAccountConfirmMessage": "Spowoduje to trwałe usunięcie konta, wszystkich organizacji, które posiadasz, oraz wszystkich danych w tych organizacjach. Tej operacji nie można cofnąć.",
|
||||
"deleteAccountConfirmString": "usuń konto",
|
||||
"deleteAccountSuccess": "Konto usunięte",
|
||||
"deleteAccountSuccessMessage": "Twoje konto zostało usunięte.",
|
||||
"deleteAccountError": "Nie udało się usunąć konta",
|
||||
"deleteAccountPreviewAccount": "Twoje konto",
|
||||
"deleteAccountPreviewOrgs": "Organizacje, które jesteś właścicielem (i wszystkie ich dane)",
|
||||
"orgMissing": "Brak ID organizacji",
|
||||
"orgMissingMessage": "Nie można ponownie wygenerować zaproszenia bez ID organizacji.",
|
||||
"accessUsersManage": "Zarządzaj użytkownikami",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtruj według państwa zatwierdzenia",
|
||||
"approvalListEmpty": "Brak zatwierdzeń",
|
||||
"approvalState": "Państwo zatwierdzające",
|
||||
"approvalLoadMore": "Załaduj więcej",
|
||||
"loadingApprovals": "Wczytywanie zatwierdzeń",
|
||||
"approve": "Zatwierdź",
|
||||
"approved": "Zatwierdzone",
|
||||
"denied": "Odmowa",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rola usunięta",
|
||||
"accessRoleRemovedDescription": "Rola została pomyślnie usunięta.",
|
||||
"accessRoleRequiredRemove": "Przed usunięciem tej roli, wybierz nową rolę do której zostaną przeniesieni obecni członkowie.",
|
||||
"network": "Sieć",
|
||||
"manage": "Zarządzaj",
|
||||
"sitesNotFound": "Nie znaleziono witryn.",
|
||||
"pangolinServerAdmin": "Administrator serwera - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Konfiguracja - Pangolin",
|
||||
"orgNameRequired": "Nazwa organizacji jest wymagana",
|
||||
"orgIdRequired": "ID organizacji jest wymagane",
|
||||
"orgIdMaxLength": "Identyfikator organizacji musi mieć co najwyżej 32 znaki",
|
||||
"orgErrorCreate": "Wystąpił błąd podczas tworzenia organizacji",
|
||||
"pageNotFound": "Nie znaleziono strony",
|
||||
"pageNotFoundDescription": "Ups! Strona, której szukasz, nie istnieje.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Zobacz dzienniki",
|
||||
"noneSelected": "Nie wybrano",
|
||||
"orgNotFound2": "Nie znaleziono organizacji.",
|
||||
"searchProgress": "Szukaj...",
|
||||
"searchPlaceholder": "Szukaj...",
|
||||
"emptySearchOptions": "Nie znaleziono opcji",
|
||||
"create": "Utwórz",
|
||||
"orgs": "Organizacje",
|
||||
"loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Prywatny",
|
||||
"sidebarAccessControl": "Kontrola dostępu",
|
||||
"sidebarLogsAndAnalytics": "Logi i Analityki",
|
||||
"sidebarTeam": "Drużyna",
|
||||
"sidebarUsers": "Użytkownicy",
|
||||
"sidebarAdmin": "Administrator",
|
||||
"sidebarInvitations": "Zaproszenia",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Dziennik & Analityka",
|
||||
"sidebarBluePrints": "Schematy",
|
||||
"sidebarOrganization": "Organizacja",
|
||||
"sidebarManagement": "Zarządzanie",
|
||||
"sidebarBillingAndLicenses": "Płatność i licencje",
|
||||
"sidebarLogsAnalytics": "Analityka",
|
||||
"blueprints": "Schematy",
|
||||
"blueprintsDescription": "Zastosuj konfiguracje deklaracyjne i wyświetl poprzednie operacje",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Przetworzona zawartość (tylko do odczytu)",
|
||||
"enableDockerSocket": "Włącz schemat dokera",
|
||||
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
|
||||
"enableDockerSocketLink": "Dowiedz się więcej",
|
||||
"viewDockerContainers": "Zobacz kontenery dokujące",
|
||||
"containersIn": "Pojemniki w {siteName}",
|
||||
"selectContainerDescription": "Wybierz dowolny kontener do użycia jako nazwa hosta dla tego celu. Kliknij port, aby użyć portu.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Witryny",
|
||||
"billingUsers": "Użytkownicy",
|
||||
"billingDomains": "Domeny",
|
||||
"billingOrganizations": "O masie całkowitej pojazdu przekraczającej 5 ton, ale nieprzekraczającej 5 ton",
|
||||
"billingRemoteExitNodes": "Zdalne węzły",
|
||||
"billingNoLimitConfigured": "Nie skonfigurowano limitu",
|
||||
"billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Niepowodzenie",
|
||||
"createNewOrgDescription": "Utwórz nową organizację",
|
||||
"organization": "Organizacja",
|
||||
"primary": "Podstawowy",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Zarządzaj kluczami bezpieczeństwa",
|
||||
"securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Czas w sekundach",
|
||||
"requireDeviceApproval": "Wymagaj zatwierdzenia urządzenia",
|
||||
"requireDeviceApprovalDescription": "Użytkownicy o tej roli potrzebują nowych urządzeń zatwierdzonych przez administratora, zanim będą mogli połączyć się i uzyskać dostęp do zasobów.",
|
||||
"sshAccess": "Dostęp SSH",
|
||||
"roleAllowSsh": "Zezwalaj na SSH",
|
||||
"roleAllowSshAllow": "Zezwól",
|
||||
"roleAllowSshDisallow": "Nie zezwalaj",
|
||||
"roleAllowSshDescription": "Zezwalaj użytkownikom z tej roli na łączenie się z zasobami za pomocą SSH. Gdy wyłączone, rola nie może korzystać z dostępu SSH.",
|
||||
"sshSudoMode": "Dostęp Sudo",
|
||||
"sshSudoModeNone": "Brak",
|
||||
"sshSudoModeNoneDescription": "Użytkownik nie może uruchamiać poleceń z sudo.",
|
||||
"sshSudoModeFull": "Pełne Sudo",
|
||||
"sshSudoModeFullDescription": "Użytkownik może uruchomić dowolne polecenie z sudo.",
|
||||
"sshSudoModeCommands": "Polecenia",
|
||||
"sshSudoModeCommandsDescription": "Użytkownik może uruchamiać tylko określone polecenia z sudo.",
|
||||
"sshSudo": "Zezwól na sudo",
|
||||
"sshSudoCommands": "Komendy Sudo",
|
||||
"sshSudoCommandsDescription": "Lista poleceń, które użytkownik może uruchamiać z sudo.",
|
||||
"sshCreateHomeDir": "Utwórz katalog domowy",
|
||||
"sshUnixGroups": "Grupy Unix",
|
||||
"sshUnixGroupsDescription": "Grupy Unix do dodania użytkownika do docelowego hosta.",
|
||||
"retryAttempts": "Próby Ponowienia",
|
||||
"expectedResponseCodes": "Oczekiwane Kody Odpowiedzi",
|
||||
"expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?",
|
||||
"authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu",
|
||||
"brandingLogoURL": "URL logo",
|
||||
"brandingLogoURLOrPath": "Adres URL logo lub ścieżka",
|
||||
"brandingLogoPathDescription": "Wprowadź adres URL lub ścieżkę lokalną.",
|
||||
"brandingLogoURLDescription": "Wprowadź publicznie dostępny adres URL do obrazu logo.",
|
||||
"brandingPrimaryColor": "Główny kolor",
|
||||
"brandingLogoWidth": "Szerokość (piksele)",
|
||||
"brandingLogoHeight": "Wysokość (piksele)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Kontrola dostępu",
|
||||
"editInternalResourceDialogAccessControlDescription": "Kontroluj, które role, użytkownicy i klienci maszyn mają dostęp do tego zasobu po połączeniu. Administratorzy zawsze mają dostęp.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Zakres portów musi być \"*\" dla wszystkich portów lub listą portów i zakresów oddzielonych przecinkami (np. \"80,443,8000-9000\"). Porty muszą znajdować się w przedziale od 1 do 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Lokalizacja",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Wybierz, gdzie działa demon uwierzytelniania SSH: na stronie (Newt) lub na zdalnym serwerze.",
|
||||
"internalResourceAuthDaemonDescription": "Uwierzytelnianie SSH obsługuje podpisywanie klucza SSH i uwierzytelnianie PAM dla tego zasobu. Wybierz, czy działa na stronie (Newt), czy na oddzielnym serwerze zdalnym. Zobacz <docsLink>dokumentację</docsLink> dla więcej.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Wybierz strategię",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Lokalizacja",
|
||||
"internalResourceAuthDaemonSite": "Na stronie",
|
||||
"internalResourceAuthDaemonSiteDescription": "Demon Auth działa na stronie (nowy).",
|
||||
"internalResourceAuthDaemonRemote": "Zdalny host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Demon Auth działa na serwerze, który nie jest stroną.",
|
||||
"internalResourceAuthDaemonPort": "Port Daemon (opcjonalnie)",
|
||||
"orgAuthWhatsThis": "Gdzie mogę znaleźć swój identyfikator organizacji?",
|
||||
"learnMore": "Dowiedz się więcej",
|
||||
"backToHome": "Wróć do strony głównej",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Selecione um protocolo",
|
||||
"resourcePortNumber": "Número da Porta",
|
||||
"resourcePortNumberDescription": "O número da porta externa para requisições de proxy.",
|
||||
"back": "Anterior",
|
||||
"cancel": "cancelar",
|
||||
"resourceConfig": "Snippets de Configuração",
|
||||
"resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o recurso TCP/UDP",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Ocorreu um erro ao apagar a organização.",
|
||||
"orgDeleted": "Organização excluída",
|
||||
"orgDeletedMessage": "A organização e seus dados foram excluídos.",
|
||||
"deleteAccount": "Excluir Conta",
|
||||
"deleteAccountDescription": "Exclua permanentemente sua conta, todas as organizações que você possui e todos os dados nessas organizações. Isso não pode ser desfeito.",
|
||||
"deleteAccountButton": "Excluir Conta",
|
||||
"deleteAccountConfirmTitle": "Excluir Conta",
|
||||
"deleteAccountConfirmMessage": "Isto limpará permanentemente sua conta, todas as organizações que você possui e todos os dados dentro dessas organizações. Isso não pode ser desfeito.",
|
||||
"deleteAccountConfirmString": "excluir conta",
|
||||
"deleteAccountSuccess": "Conta excluída",
|
||||
"deleteAccountSuccessMessage": "Sua conta foi excluída.",
|
||||
"deleteAccountError": "Falha ao excluir conta",
|
||||
"deleteAccountPreviewAccount": "Sua conta",
|
||||
"deleteAccountPreviewOrgs": "Organizações que você possui (e todos os dados deles)",
|
||||
"orgMissing": "ID da Organização Ausente",
|
||||
"orgMissingMessage": "Não é possível regenerar o convite sem um ID de organização.",
|
||||
"accessUsersManage": "Gerir Utilizadores",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Filtrar por estado de aprovação",
|
||||
"approvalListEmpty": "Sem aprovações",
|
||||
"approvalState": "Estado de aprovação",
|
||||
"approvalLoadMore": "Carregue mais",
|
||||
"loadingApprovals": "Carregando aprovações",
|
||||
"approve": "Aprovar",
|
||||
"approved": "Aceito",
|
||||
"denied": "Negado",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Função removida",
|
||||
"accessRoleRemovedDescription": "A função foi removida com sucesso.",
|
||||
"accessRoleRequiredRemove": "Antes de apagar esta função, selecione uma nova função para transferir os membros existentes.",
|
||||
"network": "Rede",
|
||||
"manage": "Gerir",
|
||||
"sitesNotFound": "Nenhum site encontrado.",
|
||||
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Configuração - Pangolin",
|
||||
"orgNameRequired": "O nome da organização é obrigatório",
|
||||
"orgIdRequired": "O ID da organização é obrigatório",
|
||||
"orgIdMaxLength": "ID da organização deve ter no máximo 32 caracteres",
|
||||
"orgErrorCreate": "Ocorreu um erro ao criar a organização",
|
||||
"pageNotFound": "Página Não Encontrada",
|
||||
"pageNotFoundDescription": "Ops! A página que você está procurando não existe.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Visualizar registros",
|
||||
"noneSelected": "Nenhum selecionado",
|
||||
"orgNotFound2": "Nenhuma organização encontrada.",
|
||||
"searchProgress": "Pesquisar...",
|
||||
"searchPlaceholder": "Buscar...",
|
||||
"emptySearchOptions": "Nenhuma opção encontrada",
|
||||
"create": "Criar",
|
||||
"orgs": "Organizações",
|
||||
"loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Privado",
|
||||
"sidebarAccessControl": "Controle de Acesso",
|
||||
"sidebarLogsAndAnalytics": "Registros e Análises",
|
||||
"sidebarTeam": "Equipe",
|
||||
"sidebarUsers": "Utilizadores",
|
||||
"sidebarAdmin": "Administrador",
|
||||
"sidebarInvitations": "Convites",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Registo & Análise",
|
||||
"sidebarBluePrints": "Diagramas",
|
||||
"sidebarOrganization": "Organização",
|
||||
"sidebarManagement": "Gestão",
|
||||
"sidebarBillingAndLicenses": "Faturamento e Licenças",
|
||||
"sidebarLogsAnalytics": "Análises",
|
||||
"blueprints": "Diagramas",
|
||||
"blueprintsDescription": "Aplicar configurações declarativas e ver execuções anteriores",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Conteúdo analisado (Somente Leitura)",
|
||||
"enableDockerSocket": "Habilitar o Diagrama Docker",
|
||||
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
|
||||
"enableDockerSocketLink": "Saiba mais",
|
||||
"viewDockerContainers": "Ver contêineres Docker",
|
||||
"containersIn": "Contêineres em {siteName}",
|
||||
"selectContainerDescription": "Selecione qualquer contêiner para usar como hostname para este alvo. Clique em uma porta para usar uma porta.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "sites",
|
||||
"billingUsers": "Utilizadores",
|
||||
"billingDomains": "Domínios",
|
||||
"billingOrganizations": "Órgãos",
|
||||
"billingRemoteExitNodes": "Nós remotos",
|
||||
"billingNoLimitConfigured": "Nenhum limite configurado",
|
||||
"billingEstimatedPeriod": "Período Estimado de Cobrança",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Falhou",
|
||||
"createNewOrgDescription": "Crie uma nova organização",
|
||||
"organization": "Organização",
|
||||
"primary": "Primário",
|
||||
"port": "Porta",
|
||||
"securityKeyManage": "Gerir chaves de segurança",
|
||||
"securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "O tempo está em segundos",
|
||||
"requireDeviceApproval": "Exigir aprovação do dispositivo",
|
||||
"requireDeviceApprovalDescription": "Usuários com esta função precisam de novos dispositivos aprovados por um administrador antes que eles possam se conectar e acessar recursos.",
|
||||
"sshAccess": "Acesso SSH",
|
||||
"roleAllowSsh": "Permitir SSH",
|
||||
"roleAllowSshAllow": "Autorizar",
|
||||
"roleAllowSshDisallow": "Anular",
|
||||
"roleAllowSshDescription": "Permitir que usuários com esta função se conectem a recursos via SSH. Quando desativado, a função não pode usar o acesso SSH.",
|
||||
"sshSudoMode": "Acesso Sudo",
|
||||
"sshSudoModeNone": "Nenhuma",
|
||||
"sshSudoModeNoneDescription": "O usuário não pode executar comandos com o sudo.",
|
||||
"sshSudoModeFull": "Sudo Completo",
|
||||
"sshSudoModeFullDescription": "O usuário pode executar qualquer comando com sudo.",
|
||||
"sshSudoModeCommands": "Comandos",
|
||||
"sshSudoModeCommandsDescription": "Usuário só pode executar os comandos especificados com sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos com permissão de executar com o sudo.",
|
||||
"sshCreateHomeDir": "Criar Diretório Inicial",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix para adicionar o usuário no host de destino.",
|
||||
"retryAttempts": "Tentativas de Repetição",
|
||||
"expectedResponseCodes": "Códigos de Resposta Esperados",
|
||||
"expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?",
|
||||
"authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca",
|
||||
"brandingLogoURL": "URL do Logo",
|
||||
"brandingLogoURLOrPath": "URL ou caminho do logotipo",
|
||||
"brandingLogoPathDescription": "Insira uma URL ou um caminho local.",
|
||||
"brandingLogoURLDescription": "Digite uma URL publicamente acessível para a sua imagem do logotipo.",
|
||||
"brandingPrimaryColor": "Cor Primária",
|
||||
"brandingLogoWidth": "Largura (px)",
|
||||
"brandingLogoHeight": "Altura (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Controle de Acesso",
|
||||
"editInternalResourceDialogAccessControlDescription": "Controle quais funções, usuários e clientes de máquina podem acessar este recurso quando conectados. Os administradores sempre têm acesso.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "O intervalo de portas deve ser \"*\" para todas as portas, ou uma lista de portas e intervalos separados por vírgulas (por exemplo, \"80,443,8000-9000\"). As portas devem estar entre 1 e 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Local do Daemon de autenticação SSH",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Escolha onde o daemon de autenticação SSH funciona: no site (Newt) ou em um host remoto.",
|
||||
"internalResourceAuthDaemonDescription": "A autenticação SSH daemon lida com assinatura de chave SSH e autenticação PAM para este recurso. Escolha se ele é executado no site (Newt) ou em um host remoto separado. Veja <docsLink>a documentação</docsLink> para mais informações.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Selecione a estratégia",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Local:",
|
||||
"internalResourceAuthDaemonSite": "No Site",
|
||||
"internalResourceAuthDaemonSiteDescription": "O serviço de autenticação é executado no site (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Host Remoto",
|
||||
"internalResourceAuthDaemonRemoteDescription": "O serviço de autenticação é executado em um host que não é o site.",
|
||||
"internalResourceAuthDaemonPort": "Porta do Daemon (opcional)",
|
||||
"orgAuthWhatsThis": "Onde posso encontrar meu ID da organização?",
|
||||
"learnMore": "Saiba mais",
|
||||
"backToHome": "Voltar para a página inicial",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Выберите протокол",
|
||||
"resourcePortNumber": "Номер порта",
|
||||
"resourcePortNumberDescription": "Внешний номер порта для проксирования запросов.",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмена",
|
||||
"resourceConfig": "Фрагменты конфигурации",
|
||||
"resourceConfigDescription": "Скопируйте и вставьте эти сниппеты для настройки TCP/UDP ресурса",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Произошла ошибка при удалении организации.",
|
||||
"orgDeleted": "Организация удалена",
|
||||
"orgDeletedMessage": "Организация и её данные были удалены.",
|
||||
"deleteAccount": "Удалить аккаунт",
|
||||
"deleteAccountDescription": "Окончательно удалить учетную запись, все организации, которые вы владеете, и все данные этих организаций не могут быть отменены.",
|
||||
"deleteAccountButton": "Удалить аккаунт",
|
||||
"deleteAccountConfirmTitle": "Удалить аккаунт",
|
||||
"deleteAccountConfirmMessage": "Это очистит ваш аккаунт, все организации, которым вы владеете, и все данные этих организаций не могут быть отменены.",
|
||||
"deleteAccountConfirmString": "удалить аккаунт",
|
||||
"deleteAccountSuccess": "Учетная запись удалена",
|
||||
"deleteAccountSuccessMessage": "Ваша учетная запись удалена.",
|
||||
"deleteAccountError": "Не удалось удалить аккаунт",
|
||||
"deleteAccountPreviewAccount": "Ваша учетная запись",
|
||||
"deleteAccountPreviewOrgs": "Организации, которые вы владеете (и все их данные)",
|
||||
"orgMissing": "Отсутствует ID организации",
|
||||
"orgMissingMessage": "Невозможно восстановить приглашение без ID организации.",
|
||||
"accessUsersManage": "Управление пользователями",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Фильтр по состоянию утверждения",
|
||||
"approvalListEmpty": "Нет утверждений",
|
||||
"approvalState": "Состояние одобрения",
|
||||
"approvalLoadMore": "Загрузить еще",
|
||||
"loadingApprovals": "Загрузка утверждений",
|
||||
"approve": "Одобрить",
|
||||
"approved": "Одобрено",
|
||||
"denied": "Отказано",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Роль удалена",
|
||||
"accessRoleRemovedDescription": "Роль была успешно удалена.",
|
||||
"accessRoleRequiredRemove": "Перед удалением этой роли выберите новую роль для переноса существующих участников.",
|
||||
"network": "Сеть",
|
||||
"manage": "Управление",
|
||||
"sitesNotFound": "Сайты не найдены.",
|
||||
"pangolinServerAdmin": "Администратор сервера - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Настройка - Pangolin",
|
||||
"orgNameRequired": "Название организации обязательно",
|
||||
"orgIdRequired": "ID организации обязателен",
|
||||
"orgIdMaxLength": "ID организации должен быть не более 32 символов",
|
||||
"orgErrorCreate": "Произошла ошибка при создании организации",
|
||||
"pageNotFound": "Страница не найдена",
|
||||
"pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Просмотр журналов",
|
||||
"noneSelected": "Ничего не выбрано",
|
||||
"orgNotFound2": "Организации не найдены.",
|
||||
"searchProgress": "Поиск...",
|
||||
"searchPlaceholder": "Поиск...",
|
||||
"emptySearchOptions": "Опции не найдены",
|
||||
"create": "Создать",
|
||||
"orgs": "Организации",
|
||||
"loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Приватный",
|
||||
"sidebarAccessControl": "Контроль доступа",
|
||||
"sidebarLogsAndAnalytics": "Журналы и аналитика",
|
||||
"sidebarTeam": "Команда",
|
||||
"sidebarUsers": "Пользователи",
|
||||
"sidebarAdmin": "Админ",
|
||||
"sidebarInvitations": "Приглашения",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Журнал и аналитика",
|
||||
"sidebarBluePrints": "Чертежи",
|
||||
"sidebarOrganization": "Организация",
|
||||
"sidebarManagement": "Управление",
|
||||
"sidebarBillingAndLicenses": "Биллинг и лицензии",
|
||||
"sidebarLogsAnalytics": "Статистика",
|
||||
"blueprints": "Чертежи",
|
||||
"blueprintsDescription": "Применить декларирующие конфигурации и просмотреть предыдущие запуски",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Переработанное содержимое (только для чтения)",
|
||||
"enableDockerSocket": "Включить чертёж Docker",
|
||||
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
||||
"enableDockerSocketLink": "Узнать больше",
|
||||
"viewDockerContainers": "Просмотр контейнеров Docker",
|
||||
"containersIn": "Контейнеры в {siteName}",
|
||||
"selectContainerDescription": "Выберите любой контейнер для использования в качестве имени хоста для этой цели. Нажмите на порт, чтобы использовать порт.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Сайты",
|
||||
"billingUsers": "Пользователи",
|
||||
"billingDomains": "Домены",
|
||||
"billingOrganizations": "Орги",
|
||||
"billingRemoteExitNodes": "Удаленные узлы",
|
||||
"billingNoLimitConfigured": "Лимит не установлен",
|
||||
"billingEstimatedPeriod": "Предполагаемый период выставления счетов",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Ошибка",
|
||||
"createNewOrgDescription": "Создать новую организацию",
|
||||
"organization": "Организация",
|
||||
"primary": "Первичный",
|
||||
"port": "Порт",
|
||||
"securityKeyManage": "Управление ключами безопасности",
|
||||
"securityKeyDescription": "Добавить или удалить ключи безопасности для аутентификации без пароля",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Время указано в секундах",
|
||||
"requireDeviceApproval": "Требовать подтверждения устройства",
|
||||
"requireDeviceApprovalDescription": "Пользователям с этой ролью нужны новые устройства, одобренные администратором, прежде чем они смогут подключаться и получать доступ к ресурсам.",
|
||||
"sshAccess": "SSH доступ",
|
||||
"roleAllowSsh": "Разрешить SSH",
|
||||
"roleAllowSshAllow": "Разрешить",
|
||||
"roleAllowSshDisallow": "Запретить",
|
||||
"roleAllowSshDescription": "Разрешить пользователям с этой ролью подключаться к ресурсам через SSH. Если отключено, роль не может использовать доступ SSH.",
|
||||
"sshSudoMode": "Sudo доступ",
|
||||
"sshSudoModeNone": "Нет",
|
||||
"sshSudoModeNoneDescription": "Пользователь не может запускать команды с sudo.",
|
||||
"sshSudoModeFull": "Полная судо",
|
||||
"sshSudoModeFullDescription": "Пользователь может запускать любую команду с помощью sudo.",
|
||||
"sshSudoModeCommands": "Команды",
|
||||
"sshSudoModeCommandsDescription": "Пользователь может запускать только указанные команды с помощью sudo.",
|
||||
"sshSudo": "Разрешить sudo",
|
||||
"sshSudoCommands": "Sudo Команды",
|
||||
"sshSudoCommandsDescription": "Список команд, которые пользователю разрешено запускать с помощью sudo.",
|
||||
"sshCreateHomeDir": "Создать домашний каталог",
|
||||
"sshUnixGroups": "Unix группы",
|
||||
"sshUnixGroupsDescription": "Unix группы для добавления пользователя на целевой хост.",
|
||||
"retryAttempts": "Количество попыток повторного запроса",
|
||||
"expectedResponseCodes": "Ожидаемые коды ответов",
|
||||
"expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?",
|
||||
"authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования",
|
||||
"brandingLogoURL": "URL логотипа",
|
||||
"brandingLogoURLOrPath": "URL логотипа или путь",
|
||||
"brandingLogoPathDescription": "Введите URL или локальный путь.",
|
||||
"brandingLogoURLDescription": "Введите публичный URL для изображения вашего логотипа.",
|
||||
"brandingPrimaryColor": "Основной цвет",
|
||||
"brandingLogoWidth": "Ширина (px)",
|
||||
"brandingLogoHeight": "Высота (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Контроль доступа",
|
||||
"editInternalResourceDialogAccessControlDescription": "Контролируйте, какие роли, пользователи и машинные клиенты имеют доступ к этому ресурсу при подключении. Администраторы всегда имеют доступ.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Диапазон портов должен быть \"*\" для всех портов или списком портов и диапазонов через запятую (например, \"80,443,8000-9000\"). Порты должны находиться в диапазоне от 1 до 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "Местоположение демона по SSH",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Выберите, где работает демон аутентификации SSH: на сайте (Newt) или на удаленном узле.",
|
||||
"internalResourceAuthDaemonDescription": "Демон аутентификации SSH обрабатывает подписание ключей SSH и аутентификацию PAM для этого ресурса. Выберите, запускать ли его на сайте (Newt) или на отдельном удаленном хосте. Подробности смотрите в <docsLink>документации</docsLink>.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Выберите стратегию",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Местоположение",
|
||||
"internalResourceAuthDaemonSite": "На сайте",
|
||||
"internalResourceAuthDaemonSiteDescription": "На сайте работает демон Auth (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Удаленный хост",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Демон Auth запускается на хост, который не является сайтом.",
|
||||
"internalResourceAuthDaemonPort": "Порт демона (опционально)",
|
||||
"orgAuthWhatsThis": "Где я могу найти ID моей организации?",
|
||||
"learnMore": "Узнать больше",
|
||||
"backToHome": "Вернуться домой",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "Bir protokol seçin",
|
||||
"resourcePortNumber": "Port Numarası",
|
||||
"resourcePortNumberDescription": "Vekil istekler için harici port numarası.",
|
||||
"back": "Geri",
|
||||
"cancel": "İptal",
|
||||
"resourceConfig": "Yapılandırma Parçaları",
|
||||
"resourceConfigDescription": "TCP/UDP kaynağınızı kurmak için bu yapılandırma parçalarını kopyalayıp yapıştırın",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "Organizasyon silinirken bir hata oluştu.",
|
||||
"orgDeleted": "Organizasyon silindi",
|
||||
"orgDeletedMessage": "Organizasyon ve verileri silindi.",
|
||||
"deleteAccount": "Hesabı Sil",
|
||||
"deleteAccountDescription": "Hesabınızı, sahip olduğunuz tüm organizasyonları ve bu organizasyonlardaki tüm verileri kalıcı olarak silin. Bu geri alınamaz.",
|
||||
"deleteAccountButton": "Hesabı Sil",
|
||||
"deleteAccountConfirmTitle": "Hesabı Sil",
|
||||
"deleteAccountConfirmMessage": "Bu işlem, hesabınızı, sahip olduğunuz tüm organizasyonları ve bu organizasyonlardaki tüm verileri kalıcı olarak silecektir. Bu geri alınamaz.",
|
||||
"deleteAccountConfirmString": "hesabı sil",
|
||||
"deleteAccountSuccess": "Hesap Silindi",
|
||||
"deleteAccountSuccessMessage": "Hesabınız silindi.",
|
||||
"deleteAccountError": "Hesabı silme başarısız oldu",
|
||||
"deleteAccountPreviewAccount": "Hesabınız",
|
||||
"deleteAccountPreviewOrgs": "Sahip olduğunuz organizasyonlar (ve tüm verileri)",
|
||||
"orgMissing": "Organizasyon Kimliği Eksik",
|
||||
"orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.",
|
||||
"accessUsersManage": "Kullanıcıları Yönet",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "Onay Durumuna Göre Filtrele",
|
||||
"approvalListEmpty": "Onay yok",
|
||||
"approvalState": "Onay Durumu",
|
||||
"approvalLoadMore": "Daha fazla yükle",
|
||||
"loadingApprovals": "Onaylar Yükleniyor",
|
||||
"approve": "Onayla",
|
||||
"approved": "Onaylandı",
|
||||
"denied": "Reddedildi",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "Rol kaldırıldı",
|
||||
"accessRoleRemovedDescription": "Rol başarıyla kaldırıldı.",
|
||||
"accessRoleRequiredRemove": "Bu rolü silmeden önce, mevcut üyeleri aktarmak için yeni bir rol seçin.",
|
||||
"network": "Ağ",
|
||||
"manage": "Yönet",
|
||||
"sitesNotFound": "Site bulunamadı.",
|
||||
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "Kurulum - Pangolin",
|
||||
"orgNameRequired": "Kuruluş adı gereklidir",
|
||||
"orgIdRequired": "Kuruluş ID gereklidir",
|
||||
"orgIdMaxLength": "Organizasyon kimliği en fazla 32 karakter olmalıdır",
|
||||
"orgErrorCreate": "Kuruluş oluşturulurken bir hata oluştu",
|
||||
"pageNotFound": "Sayfa Bulunamadı",
|
||||
"pageNotFoundDescription": "Oops! Aradığınız sayfa mevcut değil.",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "Kayıtları Görüntüle",
|
||||
"noneSelected": "Hiçbiri seçili değil",
|
||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||
"searchProgress": "Ara...",
|
||||
"searchPlaceholder": "Ara...",
|
||||
"emptySearchOptions": "Seçenek bulunamadı",
|
||||
"create": "Oluştur",
|
||||
"orgs": "Organizasyonlar",
|
||||
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "Özel",
|
||||
"sidebarAccessControl": "Erişim Kontrolü",
|
||||
"sidebarLogsAndAnalytics": "Kayıtlar & Analitik",
|
||||
"sidebarTeam": "Ekip",
|
||||
"sidebarUsers": "Kullanıcılar",
|
||||
"sidebarAdmin": "Yönetici",
|
||||
"sidebarInvitations": "Davetiye",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "Kayıt & Analiz",
|
||||
"sidebarBluePrints": "Planlar",
|
||||
"sidebarOrganization": "Organizasyon",
|
||||
"sidebarManagement": "Yönetim",
|
||||
"sidebarBillingAndLicenses": "Faturalandırma & Lisanslar",
|
||||
"sidebarLogsAnalytics": "Analitik",
|
||||
"blueprints": "Planlar",
|
||||
"blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "Verilerin Ayrıştırılmış İçeriği (Salt Okunur)",
|
||||
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
||||
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
||||
"enableDockerSocketLink": "Daha fazla bilgi",
|
||||
"viewDockerContainers": "Docker Konteynerlerini Görüntüle",
|
||||
"containersIn": "{siteName} içindeki konteynerler",
|
||||
"selectContainerDescription": "Bu hedef için bir ana bilgisayar adı olarak kullanmak üzere herhangi bir konteyner seçin. Bir bağlantı noktası kullanmak için bir bağlantı noktasına tıklayın.",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "Siteler",
|
||||
"billingUsers": "Kullanıcılar",
|
||||
"billingDomains": "Alan Adları",
|
||||
"billingOrganizations": "Organizasyonlar",
|
||||
"billingRemoteExitNodes": "Uzak Düğümler",
|
||||
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
|
||||
"billingEstimatedPeriod": "Tahmini Fatura Dönemi",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "Başarısız",
|
||||
"createNewOrgDescription": "Yeni bir organizasyon oluşturun",
|
||||
"organization": "Kuruluş",
|
||||
"primary": "Birincil",
|
||||
"port": "Bağlantı Noktası",
|
||||
"securityKeyManage": "Güvenlik Anahtarlarını Yönet",
|
||||
"securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "Zaman saniye cinsindendir",
|
||||
"requireDeviceApproval": "Cihaz Onaylarını Gerektir",
|
||||
"requireDeviceApprovalDescription": "Bu role sahip kullanıcıların yeni cihazlarının bağlanabilmesi ve kaynaklara erişebilmesi için bir yönetici tarafından onaylanması gerekiyor.",
|
||||
"sshAccess": "SSH Erişimi",
|
||||
"roleAllowSsh": "SSH'a İzin Ver",
|
||||
"roleAllowSshAllow": "İzin Ver",
|
||||
"roleAllowSshDisallow": "İzin Verme",
|
||||
"roleAllowSshDescription": "Bu role sahip kullanıcıların SSH aracılığıyla kaynaklara bağlanmasına izin verin. Devre dışı bırakıldığında, rol SSH erişimini kullanamaz.",
|
||||
"sshSudoMode": "Sudo Erişimi",
|
||||
"sshSudoModeNone": "Hiçbiri",
|
||||
"sshSudoModeNoneDescription": "Kullanıcı, sudo komutunu kullanarak komut çalıştıramaz.",
|
||||
"sshSudoModeFull": "Tam Sudo",
|
||||
"sshSudoModeFullDescription": "Kullanıcı, sudo komutuyla her türlü komutu çalıştırabilir.",
|
||||
"sshSudoModeCommands": "Komutlar",
|
||||
"sshSudoModeCommandsDescription": "Kullanıcı sadece belirtilen komutları sudo ile çalıştırabilir.",
|
||||
"sshSudo": "Sudo'ya izin ver",
|
||||
"sshSudoCommands": "Sudo Komutları",
|
||||
"sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların listesi.",
|
||||
"sshCreateHomeDir": "Ev Dizini Oluştur",
|
||||
"sshUnixGroups": "Unix Grupları",
|
||||
"sshUnixGroupsDescription": "Hedef ana bilgisayarda kullanıcıya eklemek için Unix grupları.",
|
||||
"retryAttempts": "Tekrar Deneme Girişimleri",
|
||||
"expectedResponseCodes": "Beklenen Yanıt Kodları",
|
||||
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
|
||||
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "Logo URL veya Yol",
|
||||
"brandingLogoPathDescription": "Bir URL veya yerel bir yol girin.",
|
||||
"brandingLogoURLDescription": "Logo resminiz için genel olarak erişilebilir bir URL girin.",
|
||||
"brandingPrimaryColor": "Ana Renk",
|
||||
"brandingLogoWidth": "Genişlik (px)",
|
||||
"brandingLogoHeight": "Yükseklik (px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "Erişim Kontrolü",
|
||||
"editInternalResourceDialogAccessControlDescription": "Bağlandığında bu kaynağa erişimi olan roller, kullanıcılar ve makine müşterilerini kontrol edin. Yöneticiler her zaman erişime sahiptir.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Port aralığı, tüm portlar için \"*\" veya virgülle ayrılmış bir port ve aralık listesi olmalıdır (ör. \"80,443,8000-9000\"). Portlar 1 ile 65535 arasında olmalıdır.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Kimlik Doğrulama Daemon Yeri",
|
||||
"internalResourceAuthDaemonStrategyDescription": "SSH kimlik doğrulama sunucusunun nerede çalışacağını seçin: sitede (Newt) veya uzak bir ana bilgisayarda.",
|
||||
"internalResourceAuthDaemonDescription": "SSH kimlik doğrulama sunucusu, bu kaynak için SSH anahtar imzalama ve PAM kimlik doğrulamasını yapar. Sitede (Newt) veya ayrı bir uzak ana bilgisayarda çalışıp çalışmayacağını seçin. Daha fazla bilgi için <docsLink> belgeleri</docsLink> görün.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Strateji Seçin",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Konum",
|
||||
"internalResourceAuthDaemonSite": "Sitede",
|
||||
"internalResourceAuthDaemonSiteDescription": "Kimlik doğrulama sunucusu sitede (Newt) çalışır.",
|
||||
"internalResourceAuthDaemonRemote": "Uzak Ana Bilgisayar",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Kimlik doğrulama sunucusu, site olmayan bir ana bilgisayarda çalışır.",
|
||||
"internalResourceAuthDaemonPort": "Daemon Portu (isteğe bağlı)",
|
||||
"orgAuthWhatsThis": "Kuruluş kimliğimi nerede bulabilirim?",
|
||||
"learnMore": "Daha fazla bilgi",
|
||||
"backToHome": "Ana sayfaya geri dön",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"protocolSelect": "选择协议",
|
||||
"resourcePortNumber": "端口号",
|
||||
"resourcePortNumberDescription": "代理请求的外部端口号。",
|
||||
"back": "后退",
|
||||
"cancel": "取消",
|
||||
"resourceConfig": "配置片段",
|
||||
"resourceConfigDescription": "复制并粘贴这些配置片段以设置 TCP/UDP 资源",
|
||||
@@ -246,6 +247,17 @@
|
||||
"orgErrorDeleteMessage": "删除组织时出错。",
|
||||
"orgDeleted": "组织已删除",
|
||||
"orgDeletedMessage": "组织及其数据已被删除。",
|
||||
"deleteAccount": "删除帐户",
|
||||
"deleteAccountDescription": "永久删除您的帐户、您拥有的所有组织以及这些组织中的所有数据。此操作无法撤消。",
|
||||
"deleteAccountButton": "删除帐户",
|
||||
"deleteAccountConfirmTitle": "删除帐户",
|
||||
"deleteAccountConfirmMessage": "这将永久擦除您的帐户、您拥有的所有组织以及这些组织中的所有数据。这不能撤消。",
|
||||
"deleteAccountConfirmString": "删除帐户",
|
||||
"deleteAccountSuccess": "账户已删除",
|
||||
"deleteAccountSuccessMessage": "您的帐户已被删除。",
|
||||
"deleteAccountError": "删除帐户失败",
|
||||
"deleteAccountPreviewAccount": "您的帐户",
|
||||
"deleteAccountPreviewOrgs": "您拥有的组织 (和所有数据)",
|
||||
"orgMissing": "缺少组织 ID",
|
||||
"orgMissingMessage": "没有组织ID,无法重新生成邀请。",
|
||||
"accessUsersManage": "管理用户",
|
||||
@@ -461,6 +473,8 @@
|
||||
"filterByApprovalState": "按批准状态过滤",
|
||||
"approvalListEmpty": "无批准",
|
||||
"approvalState": "审批状态",
|
||||
"approvalLoadMore": "加载更多",
|
||||
"loadingApprovals": "正在加载批准",
|
||||
"approve": "批准",
|
||||
"approved": "已批准",
|
||||
"denied": "被拒绝",
|
||||
@@ -776,6 +790,7 @@
|
||||
"accessRoleRemoved": "角色已删除",
|
||||
"accessRoleRemovedDescription": "角色已成功删除。",
|
||||
"accessRoleRequiredRemove": "删除此角色之前,请选择一个新角色来转移现有成员。",
|
||||
"network": "网络",
|
||||
"manage": "管理",
|
||||
"sitesNotFound": "未找到站点。",
|
||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||
@@ -1017,6 +1032,7 @@
|
||||
"pangolinSetup": "认证 - Pangolin",
|
||||
"orgNameRequired": "组织名称是必需的",
|
||||
"orgIdRequired": "组织ID是必需的",
|
||||
"orgIdMaxLength": "组织 ID 必须至少 32 个字符",
|
||||
"orgErrorCreate": "创建组织时出错",
|
||||
"pageNotFound": "找不到页面",
|
||||
"pageNotFoundDescription": "哎呀!您正在查找的页面不存在。",
|
||||
@@ -1169,7 +1185,8 @@
|
||||
"actionViewLogs": "查看日志",
|
||||
"noneSelected": "未选择",
|
||||
"orgNotFound2": "未找到组织。",
|
||||
"searchProgress": "搜索中...",
|
||||
"searchPlaceholder": "搜索...",
|
||||
"emptySearchOptions": "未找到选项",
|
||||
"create": "创建",
|
||||
"orgs": "组织",
|
||||
"loginError": "发生意外错误。请重试。",
|
||||
@@ -1233,6 +1250,7 @@
|
||||
"sidebarClientResources": "非公开的",
|
||||
"sidebarAccessControl": "访问控制",
|
||||
"sidebarLogsAndAnalytics": "日志与分析",
|
||||
"sidebarTeam": "团队",
|
||||
"sidebarUsers": "用户",
|
||||
"sidebarAdmin": "管理员",
|
||||
"sidebarInvitations": "邀请",
|
||||
@@ -1251,6 +1269,8 @@
|
||||
"sidebarLogAndAnalytics": "日志与分析",
|
||||
"sidebarBluePrints": "蓝图",
|
||||
"sidebarOrganization": "组织",
|
||||
"sidebarManagement": "管理",
|
||||
"sidebarBillingAndLicenses": "帐单和许可证",
|
||||
"sidebarLogsAnalytics": "分析",
|
||||
"blueprints": "蓝图",
|
||||
"blueprintsDescription": "应用声明配置并查看先前运行的",
|
||||
@@ -1272,7 +1292,6 @@
|
||||
"parsedContents": "解析内容 (只读)",
|
||||
"enableDockerSocket": "启用 Docker 蓝图",
|
||||
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
||||
"enableDockerSocketLink": "了解更多",
|
||||
"viewDockerContainers": "查看停靠容器",
|
||||
"containersIn": "{siteName} 中的容器",
|
||||
"selectContainerDescription": "选择任何容器作为目标的主机名。点击端口使用端口。",
|
||||
@@ -1412,6 +1431,7 @@
|
||||
"billingSites": "站点",
|
||||
"billingUsers": "用户",
|
||||
"billingDomains": "域",
|
||||
"billingOrganizations": "球队",
|
||||
"billingRemoteExitNodes": "远程节点",
|
||||
"billingNoLimitConfigured": "未配置限制",
|
||||
"billingEstimatedPeriod": "估计结算周期",
|
||||
@@ -1454,6 +1474,7 @@
|
||||
"failed": "失败",
|
||||
"createNewOrgDescription": "创建一个新组织",
|
||||
"organization": "组织",
|
||||
"primary": "主要的",
|
||||
"port": "端口",
|
||||
"securityKeyManage": "管理安全密钥",
|
||||
"securityKeyDescription": "添加或删除用于无密码认证的安全密钥",
|
||||
@@ -1624,6 +1645,24 @@
|
||||
"timeIsInSeconds": "时间以秒为单位",
|
||||
"requireDeviceApproval": "需要设备批准",
|
||||
"requireDeviceApprovalDescription": "具有此角色的用户需要管理员批准的新设备才能连接和访问资源。",
|
||||
"sshAccess": "SSH 访问",
|
||||
"roleAllowSsh": "允许 SSH",
|
||||
"roleAllowSshAllow": "允许",
|
||||
"roleAllowSshDisallow": "不允许",
|
||||
"roleAllowSshDescription": "允许具有此角色的用户通过 SSH 连接到资源。禁用时,角色不能使用 SSH 访问。",
|
||||
"sshSudoMode": "Sudo 访问",
|
||||
"sshSudoModeNone": "无",
|
||||
"sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
|
||||
"sshSudoModeFull": "全苏多",
|
||||
"sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
|
||||
"sshSudoModeCommands": "命令",
|
||||
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
|
||||
"sshSudo": "允许Sudo",
|
||||
"sshSudoCommands": "Sudo 命令",
|
||||
"sshSudoCommandsDescription": "允许用户使用 sudo 运行的命令列表。",
|
||||
"sshCreateHomeDir": "创建主目录",
|
||||
"sshUnixGroups": "Unix 组",
|
||||
"sshUnixGroupsDescription": "将用户添加到目标主机的Unix组。",
|
||||
"retryAttempts": "重试次数",
|
||||
"expectedResponseCodes": "期望响应代码",
|
||||
"expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。",
|
||||
@@ -1916,6 +1955,9 @@
|
||||
"authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?",
|
||||
"authPageBrandingDeleteConfirm": "确认删除品牌",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "徽标URL或路径",
|
||||
"brandingLogoPathDescription": "输入网址或本地路径。",
|
||||
"brandingLogoURLDescription": "请在您的徽标图片中输入一个可公开访问的 URL。",
|
||||
"brandingPrimaryColor": "主要颜色",
|
||||
"brandingLogoWidth": "宽度(px)",
|
||||
"brandingLogoHeight": "高度(px)",
|
||||
@@ -2481,6 +2523,17 @@
|
||||
"editInternalResourceDialogAccessControl": "访问控制",
|
||||
"editInternalResourceDialogAccessControlDescription": "控制当连接到此资源时,哪些角色、用户和机器客户端可以访问。管理员始终具有访问权。",
|
||||
"editInternalResourceDialogPortRangeValidationError": "端口范围必须为\"*\"表示所有端口,或一个用逗号分隔的端口和范围列表(例如:\"80,443,8000-9000\")。端口必须在1到65535之间。",
|
||||
"internalResourceAuthDaemonStrategy": "SSH 认证守护进程位置",
|
||||
"internalResourceAuthDaemonStrategyDescription": "选择 SSH 身份验证守护进程在哪里运行:站点(新建) 或远程主机。",
|
||||
"internalResourceAuthDaemonDescription": "SSH 身份验证守护程序处理此资源的 SSH 密钥签名和PAM 身份验证。 选择它是在站点(新建)还是在单独的远程主机上运行。请参阅 <docsLink>文档</docsLink>。",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "选择策略",
|
||||
"internalResourceAuthDaemonStrategyLabel": "地点",
|
||||
"internalResourceAuthDaemonSite": "在站点",
|
||||
"internalResourceAuthDaemonSiteDescription": "认证守护进程在站点上运行(新建)。",
|
||||
"internalResourceAuthDaemonRemote": "远程主机",
|
||||
"internalResourceAuthDaemonRemoteDescription": "认证守护进程运行在不是站点的主机上。",
|
||||
"internalResourceAuthDaemonPort": "守护进程端口(可选)",
|
||||
"orgAuthWhatsThis": "我的组织ID在哪里可以找到?",
|
||||
"learnMore": "了解更多",
|
||||
"backToHome": "返回首页",
|
||||
|
||||
@@ -232,7 +232,11 @@ export const siteResources = pgTable("siteResources", {
|
||||
aliasAddress: varchar("aliasAddress"),
|
||||
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false),
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
});
|
||||
|
||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||
@@ -372,7 +376,11 @@ export const roles = pgTable("roles", {
|
||||
isAdmin: boolean("isAdmin"),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description"),
|
||||
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
|
||||
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
|
||||
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
|
||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
|
||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||
});
|
||||
|
||||
export const roleActions = pgTable("roleActions", {
|
||||
@@ -1059,4 +1067,6 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<typeof roundTripMessageTracker>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
|
||||
@@ -257,7 +257,11 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
|
||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false)
|
||||
.default(false),
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: text("authDaemonMode")
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
});
|
||||
|
||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||
@@ -679,7 +683,13 @@ export const roles = sqliteTable("roles", {
|
||||
description: text("description"),
|
||||
requireDeviceApproval: integer("requireDeviceApproval", {
|
||||
mode: "boolean"
|
||||
}).default(false)
|
||||
}).default(false),
|
||||
sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
|
||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
||||
true
|
||||
),
|
||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||
});
|
||||
|
||||
export const roleActions = sqliteTable("roleActions", {
|
||||
|
||||
@@ -46,8 +46,6 @@ export class UsageService {
|
||||
return null;
|
||||
}
|
||||
|
||||
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
|
||||
|
||||
// Truncate value to 11 decimal places
|
||||
value = this.truncateValue(value);
|
||||
|
||||
@@ -59,6 +57,7 @@ export class UsageService {
|
||||
try {
|
||||
let usage;
|
||||
if (transaction) {
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
|
||||
usage = await this.internalAddUsage(
|
||||
orgIdToUse,
|
||||
featureId,
|
||||
@@ -67,6 +66,7 @@ export class UsageService {
|
||||
);
|
||||
} else {
|
||||
await db.transaction(async (trx) => {
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
usage = await this.internalAddUsage(
|
||||
orgIdToUse,
|
||||
featureId,
|
||||
@@ -92,7 +92,7 @@ export class UsageService {
|
||||
const delay = baseDelay + jitter;
|
||||
|
||||
logger.warn(
|
||||
`Deadlock detected for ${orgIdToUse}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
||||
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
@@ -100,7 +100,7 @@ export class UsageService {
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`,
|
||||
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
||||
error
|
||||
);
|
||||
break;
|
||||
@@ -169,7 +169,7 @@ export class UsageService {
|
||||
return;
|
||||
}
|
||||
|
||||
let orgIdToUse = await this.getBillingOrg(orgId);
|
||||
const orgIdToUse = await this.getBillingOrg(orgId);
|
||||
|
||||
try {
|
||||
// Truncate value to 11 decimal places if provided
|
||||
@@ -227,7 +227,7 @@ export class UsageService {
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
): Promise<string | null> {
|
||||
let orgIdToUse = await this.getBillingOrg(orgId);
|
||||
const orgIdToUse = await this.getBillingOrg(orgId);
|
||||
|
||||
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
|
||||
const cached = cache.get<string>(cacheKey);
|
||||
@@ -274,7 +274,7 @@ export class UsageService {
|
||||
return null;
|
||||
}
|
||||
|
||||
let orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
|
||||
const usageId = `${orgIdToUse}-${featureId}`;
|
||||
|
||||
@@ -382,7 +382,7 @@ export class UsageService {
|
||||
return false;
|
||||
}
|
||||
|
||||
let orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
|
||||
// This method should check the current usage against the limits set for the organization
|
||||
// and kick out all of the sites on the org
|
||||
|
||||
@@ -85,7 +85,9 @@ export async function deleteOrgById(
|
||||
deletedNewtIds.push(deletedNewt.newtId);
|
||||
await trx
|
||||
.delete(newtSessions)
|
||||
.where(eq(newtSessions.newtId, deletedNewt.newtId));
|
||||
.where(
|
||||
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,13 +231,15 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
|
||||
);
|
||||
}
|
||||
for (const olmId of result.olmsToTerminate) {
|
||||
sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch(
|
||||
(error) => {
|
||||
sendTerminateClient(
|
||||
0,
|
||||
OlmErrorCodes.TERMINATED_REKEYED,
|
||||
olmId
|
||||
).catch((error) => {
|
||||
logger.error(
|
||||
"Failed to send termination message to olm:",
|
||||
error
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,14 @@ export async function verifyApiKeyRoleAccess(
|
||||
);
|
||||
}
|
||||
|
||||
const { roleIds } = req.body;
|
||||
const allRoleIds =
|
||||
roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
||||
let allRoleIds: number[] = [];
|
||||
if (!isNaN(singleRoleId)) {
|
||||
// If roleId is provided in URL params, query params, or body (single), use it exclusively
|
||||
allRoleIds = [singleRoleId];
|
||||
} else if (req.body?.roleIds) {
|
||||
// Only use body.roleIds if no single roleId was provided
|
||||
allRoleIds = req.body.roleIds;
|
||||
}
|
||||
|
||||
if (allRoleIds.length === 0) {
|
||||
return next();
|
||||
|
||||
@@ -23,8 +23,14 @@ export async function verifyRoleAccess(
|
||||
);
|
||||
}
|
||||
|
||||
const roleIds = req.body?.roleIds;
|
||||
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
||||
let allRoleIds: number[] = [];
|
||||
if (!isNaN(singleRoleId)) {
|
||||
// If roleId is provided in URL params, query params, or body (single), use it exclusively
|
||||
allRoleIds = [singleRoleId];
|
||||
} else if (req.body?.roleIds) {
|
||||
// Only use body.roleIds if no single roleId was provided
|
||||
allRoleIds = req.body.roleIds;
|
||||
}
|
||||
|
||||
if (allRoleIds.length === 0) {
|
||||
return next();
|
||||
|
||||
@@ -78,7 +78,8 @@ export async function getOrgTierData(
|
||||
if (
|
||||
subscription.type === "tier1" ||
|
||||
subscription.type === "tier2" ||
|
||||
subscription.type === "tier3"
|
||||
subscription.type === "tier3" ||
|
||||
subscription.type === "enterprise"
|
||||
) {
|
||||
tier = subscription.type;
|
||||
active = true;
|
||||
|
||||
@@ -61,7 +61,10 @@ function encodeUInt64(value: bigint): Buffer {
|
||||
* Decode a string from SSH wire format at the given offset
|
||||
* Returns the string buffer and the new offset
|
||||
*/
|
||||
function decodeString(data: Buffer, offset: number): { value: Buffer; newOffset: number } {
|
||||
function decodeString(
|
||||
data: Buffer,
|
||||
offset: number
|
||||
): { value: Buffer; newOffset: number } {
|
||||
const len = data.readUInt32BE(offset);
|
||||
const value = data.subarray(offset + 4, offset + 4 + len);
|
||||
return { value, newOffset: offset + 4 + len };
|
||||
@@ -91,7 +94,9 @@ function parseOpenSSHPublicKey(pubKeyLine: string): {
|
||||
// Verify the key type in the blob matches
|
||||
const { value: blobKeyType } = decodeString(keyData, 0);
|
||||
if (blobKeyType.toString("utf8") !== keyType) {
|
||||
throw new Error(`Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`);
|
||||
throw new Error(
|
||||
`Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`
|
||||
);
|
||||
}
|
||||
|
||||
return { keyType, keyData, comment };
|
||||
@@ -238,7 +243,7 @@ export interface SignedCertificate {
|
||||
* @param comment - Optional comment for the CA public key
|
||||
* @returns CA key pair and configuration info
|
||||
*/
|
||||
export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
|
||||
export function generateCA(comment: string = "pangolin-ssh-ca"): CAKeyPair {
|
||||
// Generate Ed25519 key pair
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
@@ -307,7 +312,10 @@ export async function getOrgCAKeys(
|
||||
key: privateKeyPem,
|
||||
format: "pem"
|
||||
});
|
||||
const publicKeyPem = pubKeyObj.export({ type: "spki", format: "pem" }) as string;
|
||||
const publicKeyPem = pubKeyObj.export({
|
||||
type: "spki",
|
||||
format: "pem"
|
||||
}) as string;
|
||||
|
||||
return {
|
||||
privateKeyPem,
|
||||
@@ -365,8 +373,8 @@ export function signPublicKey(
|
||||
const serial = options.serial ?? BigInt(Date.now());
|
||||
const certType = options.certType ?? 1; // 1 = user cert
|
||||
const now = BigInt(Math.floor(Date.now() / 1000));
|
||||
const validAfter = options.validAfter ?? (now - 60n); // 1 minute ago
|
||||
const validBefore = options.validBefore ?? (now + 86400n * 365n); // 1 year from now
|
||||
const validAfter = options.validAfter ?? now - 60n; // 1 minute ago
|
||||
const validBefore = options.validBefore ?? now + 86400n * 365n; // 1 year from now
|
||||
|
||||
// Default extensions for user certificates
|
||||
const defaultExtensions = [
|
||||
@@ -422,10 +430,7 @@ export function signPublicKey(
|
||||
]);
|
||||
|
||||
// Build complete certificate
|
||||
const certificate = Buffer.concat([
|
||||
certBody,
|
||||
encodeString(signatureBlob)
|
||||
]);
|
||||
const certificate = Buffer.concat([certBody, encodeString(signatureBlob)]);
|
||||
|
||||
// Format as OpenSSH certificate line
|
||||
const certLine = `${certTypeString} ${certificate.toString("base64")} ${options.keyId}`;
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
loginPageOrg,
|
||||
orgs,
|
||||
resources,
|
||||
roles
|
||||
roles,
|
||||
siteResources
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
@@ -286,6 +287,10 @@ async function disableFeature(
|
||||
await disableAutoProvisioning(orgId);
|
||||
break;
|
||||
|
||||
case TierFeature.SshPam:
|
||||
await disableSshPam(orgId);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn(
|
||||
`Unknown feature ${feature} for org ${orgId}, skipping`
|
||||
@@ -315,6 +320,12 @@ async function disableDeviceApprovals(orgId: string): Promise<void> {
|
||||
logger.info(`Disabled device approvals on all roles for org ${orgId}`);
|
||||
}
|
||||
|
||||
async function disableSshPam(orgId: string): Promise<void> {
|
||||
logger.info(
|
||||
`Disabled SSH PAM options on all roles and site resources for org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
async function disableLoginPageBranding(orgId: string): Promise<void> {
|
||||
const [existingBranding] = await db
|
||||
.select()
|
||||
|
||||
@@ -514,7 +514,7 @@ authenticated.post(
|
||||
verifyValidSubscription(tierMatrix.sshPam),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
// verifyUserHasAction(ActionsEnum.signSshKey),
|
||||
verifyUserHasAction(ActionsEnum.signSshKey),
|
||||
logActionAudit(ActionsEnum.signSshKey),
|
||||
ssh.signSshKey
|
||||
);
|
||||
|
||||
@@ -13,7 +13,17 @@
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, newts, orgs, roundTripMessageTracker, siteResources, sites, userOrgs } from "@server/db";
|
||||
import {
|
||||
db,
|
||||
newts,
|
||||
roles,
|
||||
roundTripMessageTracker,
|
||||
siteResources,
|
||||
sites,
|
||||
userOrgs
|
||||
} from "@server/db";
|
||||
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -135,11 +145,26 @@ export async function signSshKey(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix.sshPam
|
||||
);
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"SSH key signing requires a paid plan"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let usernameToUse;
|
||||
if (!userOrg.pamUsername) {
|
||||
if (req.user?.email) {
|
||||
// Extract username from email (first part before @)
|
||||
usernameToUse = req.user?.email.split("@")[0];
|
||||
usernameToUse = req.user?.email
|
||||
.split("@")[0]
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "");
|
||||
if (!usernameToUse) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -301,6 +326,29 @@ export async function signSshKey(
|
||||
);
|
||||
}
|
||||
|
||||
const [roleRow] = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
.limit(1);
|
||||
|
||||
let parsedSudoCommands: string[] = [];
|
||||
let parsedGroups: string[] = [];
|
||||
try {
|
||||
parsedSudoCommands = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
|
||||
if (!Array.isArray(parsedSudoCommands)) parsedSudoCommands = [];
|
||||
} catch {
|
||||
parsedSudoCommands = [];
|
||||
}
|
||||
try {
|
||||
parsedGroups = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
|
||||
if (!Array.isArray(parsedGroups)) parsedGroups = [];
|
||||
} catch {
|
||||
parsedGroups = [];
|
||||
}
|
||||
const homedir = roleRow?.sshCreateHomeDir ?? null;
|
||||
const sudoMode = roleRow?.sshSudoMode ?? "none";
|
||||
|
||||
// get the site
|
||||
const [newt] = await db
|
||||
.select()
|
||||
@@ -334,7 +382,7 @@ export async function signSshKey(
|
||||
.values({
|
||||
wsClientId: newt.newtId,
|
||||
messageType: `newt/pam/connection`,
|
||||
sentAt: Math.floor(Date.now() / 1000),
|
||||
sentAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -352,14 +400,17 @@ export async function signSshKey(
|
||||
data: {
|
||||
messageId: message.messageId,
|
||||
orgId: orgId,
|
||||
agentPort: 22123,
|
||||
agentPort: resource.authDaemonPort ?? 22123,
|
||||
externalAuthDaemon: resource.authDaemonMode === "remote",
|
||||
agentHost: resource.destination,
|
||||
caCert: caKeys.publicKeyOpenSSH,
|
||||
username: usernameToUse,
|
||||
niceId: resource.niceId,
|
||||
metadata: {
|
||||
sudo: true, // we are hardcoding these for now but should make configurable from the role or something
|
||||
homedir: true
|
||||
sudoMode: sudoMode,
|
||||
sudoCommands: parsedSudoCommands,
|
||||
homedir: homedir,
|
||||
groups: parsedGroups
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,10 +15,13 @@ import {
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import { verifyTotpCode } from "@server/auth/totp";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { build } from "@server/build";
|
||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||
import {
|
||||
deleteOrgById,
|
||||
sendTerminationMessages
|
||||
} from "@server/lib/deleteOrg";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
const deleteMyAccountBody = z.strictObject({
|
||||
password: z.string().optional(),
|
||||
@@ -230,7 +233,10 @@ export async function deleteMyAccount(
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
@@ -93,7 +93,8 @@ export async function updateClient(
|
||||
.where(
|
||||
and(
|
||||
eq(clients.niceId, niceId),
|
||||
eq(clients.orgId, clients.orgId)
|
||||
eq(clients.orgId, clients.orgId),
|
||||
ne(clients.clientId, clientId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
@@ -197,7 +197,6 @@ export async function updateSiteBandwidth(
|
||||
usageService
|
||||
.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
FeatureId.EGRESS_DATA_MB,
|
||||
bandwidthUsage
|
||||
)
|
||||
|
||||
@@ -181,7 +181,10 @@ export async function createOrg(
|
||||
}
|
||||
|
||||
if (build == "saas" && billingOrgIdForNewOrg) {
|
||||
const usage = await usageService.getUsage(billingOrgIdForNewOrg, FeatureId.ORGINIZATIONS);
|
||||
const usage = await usageService.getUsage(
|
||||
billingOrgIdForNewOrg,
|
||||
FeatureId.ORGINIZATIONS
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -218,11 +221,6 @@ export async function createOrg(
|
||||
.from(domains)
|
||||
.where(eq(domains.configManaged, true));
|
||||
|
||||
// Generate SSH CA keys for the org
|
||||
// const ca = generateCA(`${orgId}-ca`);
|
||||
// const encryptionKey = config.getRawConfig().server.secret!;
|
||||
// const encryptedCaPrivateKey = encrypt(ca.privateKeyPem, encryptionKey);
|
||||
|
||||
const saasBillingFields =
|
||||
build === "saas" && req.user && isFirstOrg !== null
|
||||
? isFirstOrg
|
||||
@@ -233,6 +231,19 @@ export async function createOrg(
|
||||
}
|
||||
: {};
|
||||
|
||||
const encryptionKey = config.getRawConfig().server.secret;
|
||||
let sshCaFields: {
|
||||
sshCaPrivateKey?: string;
|
||||
sshCaPublicKey?: string;
|
||||
} = {};
|
||||
if (encryptionKey) {
|
||||
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||
sshCaFields = {
|
||||
sshCaPrivateKey: encrypt(ca.privateKeyPem, encryptionKey),
|
||||
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||
};
|
||||
}
|
||||
|
||||
const newOrg = await trx
|
||||
.insert(orgs)
|
||||
.values({
|
||||
@@ -241,8 +252,7 @@ export async function createOrg(
|
||||
subnet,
|
||||
utilitySubnet,
|
||||
createdAt: new Date().toISOString(),
|
||||
// sshCaPrivateKey: encryptedCaPrivateKey,
|
||||
// sshCaPublicKey: ca.publicKeyOpenSSH,
|
||||
...sshCaFields,
|
||||
...saasBillingFields
|
||||
})
|
||||
.returning();
|
||||
@@ -262,7 +272,8 @@ export async function createOrg(
|
||||
orgId: newOrg[0].orgId,
|
||||
isAdmin: true,
|
||||
name: "Admin",
|
||||
description: "Admin role with the most permissions"
|
||||
description: "Admin role with the most permissions",
|
||||
sshSudoMode: "full"
|
||||
})
|
||||
.returning({ roleId: roles.roleId });
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Resource,
|
||||
resources
|
||||
} from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -33,7 +33,15 @@ const updateResourceParamsSchema = z.strictObject({
|
||||
const updateHttpResourceBodySchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
|
||||
niceId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9-]+$/,
|
||||
"niceId can only contain letters, numbers, and dashes"
|
||||
)
|
||||
.optional(),
|
||||
subdomain: subdomainSchema.nullable().optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
sso: z.boolean().optional(),
|
||||
@@ -248,14 +256,13 @@ async function updateHttpResource(
|
||||
.where(
|
||||
and(
|
||||
eq(resources.niceId, updateData.niceId),
|
||||
eq(resources.orgId, resource.orgId)
|
||||
eq(resources.orgId, resource.orgId),
|
||||
ne(resources.resourceId, resource.resourceId) // exclude the current resource from the search
|
||||
)
|
||||
);
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (
|
||||
existingResource &&
|
||||
existingResource.resourceId !== resource.resourceId
|
||||
) {
|
||||
if (existingResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
@@ -343,7 +350,10 @@ async function updateHttpResource(
|
||||
headers = null;
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.maintencePage
|
||||
);
|
||||
if (!isLicensed) {
|
||||
updateData.maintenanceModeEnabled = undefined;
|
||||
updateData.maintenanceModeType = undefined;
|
||||
|
||||
@@ -18,10 +18,17 @@ const createRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
|
||||
|
||||
const createRoleSchema = z.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
requireDeviceApproval: z.boolean().optional(),
|
||||
allowSsh: z.boolean().optional(),
|
||||
sshSudoMode: sshSudoModeSchema.optional(),
|
||||
sshSudoCommands: z.array(z.string()).optional(),
|
||||
sshCreateHomeDir: z.boolean().optional(),
|
||||
sshUnixGroups: z.array(z.string()).optional()
|
||||
});
|
||||
|
||||
export const defaultRoleAllowedActions: ActionsEnum[] = [
|
||||
@@ -101,24 +108,40 @@ export async function createRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensed) {
|
||||
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensedDeviceApprovals) {
|
||||
roleData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
|
||||
const roleInsertValues: Record<string, unknown> = {
|
||||
name: roleData.name,
|
||||
orgId
|
||||
};
|
||||
if (roleData.description !== undefined) roleInsertValues.description = roleData.description;
|
||||
if (roleData.requireDeviceApproval !== undefined) roleInsertValues.requireDeviceApproval = roleData.requireDeviceApproval;
|
||||
if (isLicensedSshPam) {
|
||||
if (roleData.sshSudoMode !== undefined) roleInsertValues.sshSudoMode = roleData.sshSudoMode;
|
||||
if (roleData.sshSudoCommands !== undefined) roleInsertValues.sshSudoCommands = JSON.stringify(roleData.sshSudoCommands);
|
||||
if (roleData.sshCreateHomeDir !== undefined) roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir;
|
||||
if (roleData.sshUnixGroups !== undefined) roleInsertValues.sshUnixGroups = JSON.stringify(roleData.sshUnixGroups);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const newRole = await trx
|
||||
.insert(roles)
|
||||
.values({
|
||||
...roleData,
|
||||
orgId
|
||||
})
|
||||
.values(roleInsertValues as typeof roles.$inferInsert)
|
||||
.returning();
|
||||
|
||||
const actionsToInsert = [...defaultRoleAllowedActions];
|
||||
if (roleData.allowSsh) {
|
||||
actionsToInsert.push(ActionsEnum.signSshKey);
|
||||
}
|
||||
|
||||
await trx
|
||||
.insert(roleActions)
|
||||
.values(
|
||||
defaultRoleAllowedActions.map((action) => ({
|
||||
actionsToInsert.map((action) => ({
|
||||
roleId: newRole[0].roleId,
|
||||
actionId: action,
|
||||
orgId
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { db, orgs, roles } from "@server/db";
|
||||
import { db, orgs, roleActions, roles } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
@@ -37,7 +38,11 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
|
||||
name: roles.name,
|
||||
description: roles.description,
|
||||
orgName: orgs.name,
|
||||
requireDeviceApproval: roles.requireDeviceApproval
|
||||
requireDeviceApproval: roles.requireDeviceApproval,
|
||||
sshSudoMode: roles.sshSudoMode,
|
||||
sshSudoCommands: roles.sshSudoCommands,
|
||||
sshCreateHomeDir: roles.sshCreateHomeDir,
|
||||
sshUnixGroups: roles.sshUnixGroups
|
||||
})
|
||||
.from(roles)
|
||||
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
|
||||
@@ -106,9 +111,28 @@ export async function listRoles(
|
||||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
let rolesWithAllowSsh = rolesList;
|
||||
if (rolesList.length > 0) {
|
||||
const roleIds = rolesList.map((r) => r.roleId);
|
||||
const signSshKeyRows = await db
|
||||
.select({ roleId: roleActions.roleId })
|
||||
.from(roleActions)
|
||||
.where(
|
||||
and(
|
||||
inArray(roleActions.roleId, roleIds),
|
||||
eq(roleActions.actionId, ActionsEnum.signSshKey)
|
||||
)
|
||||
);
|
||||
const roleIdsWithSsh = new Set(signSshKeyRows.map((r) => r.roleId));
|
||||
rolesWithAllowSsh = rolesList.map((r) => ({
|
||||
...r,
|
||||
allowSsh: roleIdsWithSsh.has(r.roleId)
|
||||
}));
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
roles: rolesList,
|
||||
roles: rolesWithAllowSsh,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, type Role } from "@server/db";
|
||||
import { roles } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { roleActions, roles } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -16,11 +17,18 @@ const updateRoleParamsSchema = z.strictObject({
|
||||
roleId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
|
||||
|
||||
const updateRoleBodySchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
requireDeviceApproval: z.boolean().optional(),
|
||||
allowSsh: z.boolean().optional(),
|
||||
sshSudoMode: sshSudoModeSchema.optional(),
|
||||
sshSudoCommands: z.array(z.string()).optional(),
|
||||
sshCreateHomeDir: z.boolean().optional(),
|
||||
sshUnixGroups: z.array(z.string()).optional()
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
error: "At least one field must be provided for update"
|
||||
@@ -75,7 +83,9 @@ export async function updateRole(
|
||||
}
|
||||
|
||||
const { roleId } = parsedParams.data;
|
||||
const updateData = parsedBody.data;
|
||||
const body = parsedBody.data;
|
||||
const { allowSsh, ...restBody } = body;
|
||||
const updateData: Record<string, unknown> = { ...restBody };
|
||||
|
||||
const role = await db
|
||||
.select()
|
||||
@@ -92,16 +102,14 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
if (role[0].isAdmin) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Cannot update a Admin role`
|
||||
)
|
||||
);
|
||||
const orgId = role[0].orgId;
|
||||
const isAdminRole = role[0].isAdmin;
|
||||
|
||||
if (isAdminRole) {
|
||||
delete updateData.name;
|
||||
delete updateData.description;
|
||||
}
|
||||
|
||||
const orgId = role[0].orgId;
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -111,18 +119,70 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensed) {
|
||||
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensedDeviceApprovals) {
|
||||
updateData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
const updatedRole = await db
|
||||
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
|
||||
if (!isLicensedSshPam) {
|
||||
delete updateData.sshSudoMode;
|
||||
delete updateData.sshSudoCommands;
|
||||
delete updateData.sshCreateHomeDir;
|
||||
delete updateData.sshUnixGroups;
|
||||
} else {
|
||||
if (Array.isArray(updateData.sshSudoCommands)) {
|
||||
updateData.sshSudoCommands = JSON.stringify(updateData.sshSudoCommands);
|
||||
}
|
||||
if (Array.isArray(updateData.sshUnixGroups)) {
|
||||
updateData.sshUnixGroups = JSON.stringify(updateData.sshUnixGroups);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedRole = await db.transaction(async (trx) => {
|
||||
const result = await trx
|
||||
.update(roles)
|
||||
.set(updateData)
|
||||
.set(updateData as typeof roles.$inferInsert)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
.returning();
|
||||
|
||||
if (updatedRole.length === 0) {
|
||||
if (result.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allowSsh === true) {
|
||||
const existing = await trx
|
||||
.select()
|
||||
.from(roleActions)
|
||||
.where(
|
||||
and(
|
||||
eq(roleActions.roleId, roleId),
|
||||
eq(roleActions.actionId, ActionsEnum.signSshKey)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
await trx.insert(roleActions).values({
|
||||
roleId,
|
||||
actionId: ActionsEnum.signSshKey,
|
||||
orgId: orgId!
|
||||
});
|
||||
}
|
||||
} else if (allowSsh === false) {
|
||||
await trx
|
||||
.delete(roleActions)
|
||||
.where(
|
||||
and(
|
||||
eq(roleActions.roleId, roleId),
|
||||
eq(roleActions.actionId, ActionsEnum.signSshKey)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return result[0];
|
||||
});
|
||||
|
||||
if (!updatedRole) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -132,7 +192,7 @@ export async function updateRole(
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedRole[0],
|
||||
data: updatedRole,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Role updated successfully",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { sites } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -19,8 +19,8 @@ const updateSiteBodySchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
remoteSubnets: z.string().optional()
|
||||
dockerSocketEnabled: z.boolean().optional()
|
||||
// remoteSubnets: z.string().optional()
|
||||
// subdomain: z
|
||||
// .string()
|
||||
// .min(1)
|
||||
@@ -86,18 +86,19 @@ export async function updateSite(
|
||||
|
||||
// if niceId is provided, check if it's already in use by another site
|
||||
if (updateData.niceId) {
|
||||
const existingSite = await db
|
||||
const [existingSite] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, updateData.niceId),
|
||||
eq(sites.orgId, sites.orgId)
|
||||
eq(sites.orgId, sites.orgId),
|
||||
ne(sites.siteId, siteId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingSite.length > 0 && existingSite[0].siteId !== siteId) {
|
||||
if (existingSite) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
@@ -107,22 +108,22 @@ export async function updateSite(
|
||||
}
|
||||
}
|
||||
|
||||
// if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
||||
if (updateData.remoteSubnets) {
|
||||
const subnets = updateData.remoteSubnets
|
||||
.split(",")
|
||||
.map((s) => s.trim());
|
||||
for (const subnet of subnets) {
|
||||
if (!isValidCIDR(subnet)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Invalid CIDR format: ${subnet}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
||||
// if (updateData.remoteSubnets) {
|
||||
// const subnets = updateData.remoteSubnets
|
||||
// .split(",")
|
||||
// .map((s) => s.trim());
|
||||
// for (const subnet of subnets) {
|
||||
// if (!isValidCIDR(subnet)) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.BAD_REQUEST,
|
||||
// `Invalid CIDR format: ${subnet}`
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const updatedSite = await db
|
||||
.update(sites)
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
isIpInCidr,
|
||||
portRangeStringSchema
|
||||
} from "@server/lib/ip";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
@@ -53,7 +55,9 @@ const createSiteResourceSchema = z
|
||||
clientIds: z.array(z.int()),
|
||||
tcpPortRangeString: portRangeStringSchema,
|
||||
udpPortRangeString: portRangeStringSchema,
|
||||
disableIcmp: z.boolean().optional()
|
||||
disableIcmp: z.boolean().optional(),
|
||||
authDaemonPort: z.int().positive().optional(),
|
||||
authDaemonMode: z.enum(["site", "remote"]).optional()
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
@@ -168,7 +172,9 @@ export async function createSiteResource(
|
||||
clientIds,
|
||||
tcpPortRangeString,
|
||||
udpPortRangeString,
|
||||
disableIcmp
|
||||
disableIcmp,
|
||||
authDaemonPort,
|
||||
authDaemonMode
|
||||
} = parsedBody.data;
|
||||
|
||||
// Verify the site exists and belongs to the org
|
||||
@@ -267,6 +273,11 @@ export async function createSiteResource(
|
||||
}
|
||||
}
|
||||
|
||||
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix.sshPam
|
||||
);
|
||||
|
||||
const niceId = await getUniqueSiteResourceName(orgId);
|
||||
let aliasAddress: string | null = null;
|
||||
if (mode == "host") {
|
||||
@@ -277,17 +288,12 @@ export async function createSiteResource(
|
||||
let newSiteResource: SiteResource | undefined;
|
||||
await db.transaction(async (trx) => {
|
||||
// Create the site resource
|
||||
[newSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
const insertValues: typeof siteResources.$inferInsert = {
|
||||
siteId,
|
||||
niceId,
|
||||
orgId,
|
||||
name,
|
||||
mode: mode as "host" | "cidr",
|
||||
// protocol: mode === "port" ? protocol : null,
|
||||
// proxyPort: mode === "port" ? proxyPort : null,
|
||||
// destinationPort: mode === "port" ? destinationPort : null,
|
||||
destination,
|
||||
enabled,
|
||||
alias,
|
||||
@@ -295,7 +301,16 @@ export async function createSiteResource(
|
||||
tcpPortRangeString,
|
||||
udpPortRangeString,
|
||||
disableIcmp
|
||||
})
|
||||
};
|
||||
if (isLicensedSshPam) {
|
||||
if (authDaemonPort !== undefined)
|
||||
insertValues.authDaemonPort = authDaemonPort;
|
||||
if (authDaemonMode !== undefined)
|
||||
insertValues.authDaemonMode = authDaemonMode;
|
||||
}
|
||||
[newSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values(insertValues)
|
||||
.returning();
|
||||
|
||||
const siteResourceId = newSiteResource.siteResourceId;
|
||||
|
||||
@@ -78,6 +78,8 @@ function querySiteResourcesBase() {
|
||||
tcpPortRangeString: siteResources.tcpPortRangeString,
|
||||
udpPortRangeString: siteResources.udpPortRangeString,
|
||||
disableIcmp: siteResources.disableIcmp,
|
||||
authDaemonMode: siteResources.authDaemonMode,
|
||||
authDaemonPort: siteResources.authDaemonPort,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteAddress: sites.address
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
getClientSiteResourceAccess,
|
||||
rebuildClientAssociationsFromSiteResource
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const updateSiteResourceParamsSchema = z.strictObject({
|
||||
siteResourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -61,7 +63,9 @@ const updateSiteResourceSchema = z
|
||||
clientIds: z.array(z.int()),
|
||||
tcpPortRangeString: portRangeStringSchema,
|
||||
udpPortRangeString: portRangeStringSchema,
|
||||
disableIcmp: z.boolean().optional()
|
||||
disableIcmp: z.boolean().optional(),
|
||||
authDaemonPort: z.int().positive().nullish(),
|
||||
authDaemonMode: z.enum(["site", "remote"]).optional()
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
@@ -172,7 +176,9 @@ export async function updateSiteResource(
|
||||
clientIds,
|
||||
tcpPortRangeString,
|
||||
udpPortRangeString,
|
||||
disableIcmp
|
||||
disableIcmp,
|
||||
authDaemonPort,
|
||||
authDaemonMode
|
||||
} = parsedBody.data;
|
||||
|
||||
const [site] = await db
|
||||
@@ -198,6 +204,11 @@ export async function updateSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||
existingSiteResource.orgId,
|
||||
tierMatrix.sshPam
|
||||
);
|
||||
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
@@ -308,6 +319,18 @@ export async function updateSiteResource(
|
||||
// wait some time to allow for messages to be handled
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined || authDaemonMode !== undefined)
|
||||
? {
|
||||
...(authDaemonPort !== undefined && {
|
||||
authDaemonPort
|
||||
}),
|
||||
...(authDaemonMode !== undefined && {
|
||||
authDaemonMode
|
||||
})
|
||||
}
|
||||
: {};
|
||||
[updatedSiteResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
@@ -319,7 +342,8 @@ export async function updateSiteResource(
|
||||
alias: alias && alias.trim() ? alias : null,
|
||||
tcpPortRangeString: tcpPortRangeString,
|
||||
udpPortRangeString: udpPortRangeString,
|
||||
disableIcmp: disableIcmp
|
||||
disableIcmp: disableIcmp,
|
||||
...sshPamSet
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
@@ -397,6 +421,18 @@ export async function updateSiteResource(
|
||||
);
|
||||
} else {
|
||||
// Update the site resource
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined || authDaemonMode !== undefined)
|
||||
? {
|
||||
...(authDaemonPort !== undefined && {
|
||||
authDaemonPort
|
||||
}),
|
||||
...(authDaemonMode !== undefined && {
|
||||
authDaemonMode
|
||||
})
|
||||
}
|
||||
: {};
|
||||
[updatedSiteResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
@@ -408,7 +444,8 @@ export async function updateSiteResource(
|
||||
alias: alias && alias.trim() ? alias : null,
|
||||
tcpPortRangeString: tcpPortRangeString,
|
||||
udpPortRangeString: udpPortRangeString,
|
||||
disableIcmp: disableIcmp
|
||||
disableIcmp: disableIcmp,
|
||||
...sshPamSet
|
||||
})
|
||||
.where(
|
||||
and(eq(siteResources.siteResourceId, siteResourceId))
|
||||
|
||||
29
server/setup/scriptsSqlite/1.16.0.ts
Normal file
29
server/setup/scriptsSqlite/1.16.0.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const version = "1.16.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const db = new Database(location);
|
||||
|
||||
// set all admin role sudo to "full"; all other roles to "none"
|
||||
// all roles set hoemdir to true
|
||||
|
||||
// generate ca certs for all orgs?
|
||||
// set authDaemonMode to "site" for all orgs
|
||||
|
||||
try {
|
||||
db.transaction(() => {})();
|
||||
|
||||
console.log(`Migrated database`);
|
||||
} catch (e) {
|
||||
console.log("Failed to migrate db:", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
/>
|
||||
|
||||
<ClientProvider client={client}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<ClientInfoCard />
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default async function GeneralSettingsPage({
|
||||
description={t("orgSettingsDescription")}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<OrgInfoCard />
|
||||
<HorizontalTabs items={navItems}>
|
||||
{children}
|
||||
|
||||
@@ -74,7 +74,9 @@ export default async function ClientResourcesPage(
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||
disableIcmp: siteResource.disableIcmp || false
|
||||
disableIcmp: siteResource.disableIcmp || false,
|
||||
authDaemonMode: siteResource.authDaemonMode ?? null,
|
||||
authDaemonPort: siteResource.authDaemonPort ?? null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -32,8 +32,8 @@ import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useState } from "react";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
@@ -187,21 +187,22 @@ export default function GeneralPage() {
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"enableDockerSocketDescription"
|
||||
)}{" "}
|
||||
<Link
|
||||
{t.rich(
|
||||
"enableDockerSocketDescription",
|
||||
{
|
||||
docsLink: (chunks) => (
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/configure-site#docker-socket-integration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<span>
|
||||
{t(
|
||||
"enableDockerSocketLink"
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
/>
|
||||
|
||||
<SiteProvider site={site}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<SiteInfoCard />
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</div>
|
||||
|
||||
@@ -82,13 +82,13 @@ export default async function RootLayout({
|
||||
<body className={`${font.className} h-screen-safe overflow-hidden`}>
|
||||
<StoreInternalRedirect />
|
||||
<TopLoader />
|
||||
{build === "saas" && (
|
||||
{/* build === "saas" && (
|
||||
<Script
|
||||
src="https://rybbit.fossorial.io/api/script.js"
|
||||
data-site-id="fe1ff2a33287"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
)*/}
|
||||
<ViewportHeightFix />
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider
|
||||
@@ -125,9 +125,9 @@ export default async function RootLayout({
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
{/*process.env.NODE_ENV === "development" && (
|
||||
<TailwindIndicator />
|
||||
)}
|
||||
)*/}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SidebarNavItem } from "@app/components/SidebarNav";
|
||||
import { Env } from "@app/lib/types/env";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
Building2,
|
||||
ChartLine,
|
||||
Combine,
|
||||
CreditCard,
|
||||
@@ -11,10 +12,11 @@ import {
|
||||
KeyRound,
|
||||
Laptop,
|
||||
Link as LinkIcon,
|
||||
Logs, // Added from 'dev' branch
|
||||
Logs,
|
||||
MonitorUp,
|
||||
Plug,
|
||||
ReceiptText,
|
||||
ScanEye, // Added from 'dev' branch
|
||||
ScanEye,
|
||||
Server,
|
||||
Settings,
|
||||
SquareMousePointer,
|
||||
@@ -49,12 +51,12 @@ export const orgNavSections = (
|
||||
options?: OrgNavSectionsOptions
|
||||
): SidebarNavSection[] => [
|
||||
{
|
||||
heading: "sidebarGeneral",
|
||||
heading: "network",
|
||||
items: [
|
||||
{
|
||||
title: "sidebarSites",
|
||||
href: "/{orgId}/settings/sites",
|
||||
icon: <Combine className="size-4 flex-none" />
|
||||
icon: <Plug className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarResources",
|
||||
@@ -108,14 +110,19 @@ export const orgNavSections = (
|
||||
heading: "access",
|
||||
items: [
|
||||
{
|
||||
title: "sidebarUsers",
|
||||
icon: <User className="size-4 flex-none" />,
|
||||
title: "sidebarTeam",
|
||||
icon: <Users className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarUsers",
|
||||
href: "/{orgId}/settings/access/users",
|
||||
icon: <User className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarRoles",
|
||||
href: "/{orgId}/settings/access/roles",
|
||||
icon: <Users className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarInvitations",
|
||||
href: "/{orgId}/settings/access/invitations",
|
||||
@@ -123,11 +130,6 @@ export const orgNavSections = (
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "sidebarRoles",
|
||||
href: "/{orgId}/settings/access/roles",
|
||||
icon: <Users className="size-4 flex-none" />
|
||||
},
|
||||
// PaidFeaturesAlert
|
||||
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
|
||||
build === "saas" ||
|
||||
@@ -158,13 +160,23 @@ export const orgNavSections = (
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "sidebarLogsAndAnalytics",
|
||||
items: (() => {
|
||||
const logItems: SidebarNavItem[] = [
|
||||
heading: "sidebarOrganization",
|
||||
items: [
|
||||
{
|
||||
title: "sidebarLogsAndAnalytics",
|
||||
icon: <ChartLine className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarLogsAnalytics",
|
||||
href: "/{orgId}/settings/logs/analytics",
|
||||
icon: <ChartLine className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarLogsRequest",
|
||||
href: "/{orgId}/settings/logs/request",
|
||||
icon: <SquareMousePointer className="size-4 flex-none" />
|
||||
icon: (
|
||||
<SquareMousePointer className="size-4 flex-none" />
|
||||
)
|
||||
},
|
||||
...(!env?.flags.disableEnterpriseFeatures
|
||||
? [
|
||||
@@ -180,32 +192,11 @@ export const orgNavSections = (
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
const analytics = {
|
||||
title: "sidebarLogsAnalytics",
|
||||
href: "/{orgId}/settings/logs/analytics",
|
||||
icon: <ChartLine className="h-4 w-4" />
|
||||
};
|
||||
|
||||
// If only one log item, return it directly without grouping
|
||||
if (logItems.length === 1) {
|
||||
return [analytics, ...logItems];
|
||||
}
|
||||
|
||||
// If multiple log items, create a group
|
||||
return [
|
||||
analytics,
|
||||
{
|
||||
title: "sidebarLogs",
|
||||
icon: <Logs className="size-4 flex-none" />,
|
||||
items: logItems
|
||||
}
|
||||
];
|
||||
})()
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "sidebarOrganization",
|
||||
title: "sidebarManagement",
|
||||
icon: <Building2 className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarApiKeys",
|
||||
@@ -216,33 +207,40 @@ export const orgNavSections = (
|
||||
title: "sidebarBluePrints",
|
||||
href: "/{orgId}/settings/blueprints",
|
||||
icon: <ReceiptText className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarSettings",
|
||||
href: "/{orgId}/settings/general",
|
||||
icon: <Settings className="size-4 flex-none" />
|
||||
}
|
||||
]
|
||||
},
|
||||
...(build == "saas" && options?.isPrimaryOrg
|
||||
? [
|
||||
{
|
||||
heading: "sidebarBillingAndLicenses",
|
||||
title: "sidebarBillingAndLicenses",
|
||||
icon: <CreditCard className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarBilling",
|
||||
href: "/{orgId}/settings/billing",
|
||||
icon: <CreditCard className="size-4 flex-none" />
|
||||
icon: (
|
||||
<CreditCard className="size-4 flex-none" />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "sidebarEnterpriseLicenses",
|
||||
href: "/{orgId}/settings/license",
|
||||
icon: <TicketCheck className="size-4 flex-none" />
|
||||
icon: (
|
||||
<TicketCheck className="size-4 flex-none" />
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
: []),
|
||||
{
|
||||
title: "sidebarSettings",
|
||||
href: "/{orgId}/settings/general",
|
||||
icon: <Settings className="size-4 flex-none" />
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const adminNavSections = (env?: Env): SidebarNavSection[] => [
|
||||
|
||||
@@ -149,7 +149,6 @@ export default function StepperForm() {
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9_-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 32);
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ export type InternalResourceRow = {
|
||||
tcpPortRangeString: string | null;
|
||||
udpPortRangeString: string | null;
|
||||
disableIcmp: boolean;
|
||||
authDaemonMode?: "site" | "remote" | null;
|
||||
authDaemonPort?: number | null;
|
||||
};
|
||||
|
||||
type ClientResourcesTableProps = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,31 +11,19 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||
import type {
|
||||
CreateRoleBody,
|
||||
CreateRoleResponse
|
||||
} from "@server/routers/role";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { RoleForm, type RoleFormValues } from "./RoleForm";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
@@ -52,35 +40,39 @@ export default function CreateRoleForm({
|
||||
const { org } = useOrgContext();
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string({ message: t("nameRequired") })
|
||||
.min(1)
|
||||
.max(32),
|
||||
description: z.string().max(255).optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
});
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
requireDeviceApproval: false
|
||||
}
|
||||
});
|
||||
|
||||
const [loading, startTransition] = useTransition();
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
async function onSubmit(values: RoleFormValues) {
|
||||
const payload: CreateRoleBody = {
|
||||
name: values.name,
|
||||
description: values.description || undefined,
|
||||
requireDeviceApproval: values.requireDeviceApproval,
|
||||
allowSsh: values.allowSsh
|
||||
};
|
||||
if (isPaidUser(tierMatrix.sshPam)) {
|
||||
payload.sshSudoMode = values.sshSudoMode;
|
||||
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
||||
payload.sshSudoCommands =
|
||||
values.sshSudoMode === "commands" &&
|
||||
values.sshSudoCommands?.trim()
|
||||
? values.sshSudoCommands
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (values.sshUnixGroups?.trim()) {
|
||||
payload.sshUnixGroups = values.sshUnixGroups
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
const res = await api
|
||||
.put<
|
||||
AxiosResponse<CreateRoleResponse>
|
||||
>(`/org/${org?.org.orgId}/role`, values satisfies CreateRoleBody)
|
||||
.put<AxiosResponse<CreateRoleResponse>>(
|
||||
`/org/${org?.org.orgId}/role`,
|
||||
payload
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
@@ -98,24 +90,13 @@ export default function CreateRoleForm({
|
||||
title: t("accessRoleCreated"),
|
||||
description: t("accessRoleCreatedDescription")
|
||||
});
|
||||
|
||||
if (open) {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
if (open) setOpen(false);
|
||||
afterCreate?.(res.data.data);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
|
||||
@@ -124,101 +105,12 @@ export default function CreateRoleForm({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) =>
|
||||
<RoleForm
|
||||
variant="create"
|
||||
onSubmit={(values) =>
|
||||
startTransition(() => onSubmit(values))
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="create-role-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("accessRoleName")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("description")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.deviceApprovals}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireDeviceApproval"
|
||||
render={({ field }) => (
|
||||
<FormItem className="my-2">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.deviceApprovals
|
||||
)
|
||||
}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) => {
|
||||
if (
|
||||
checked !==
|
||||
"indeterminate"
|
||||
) {
|
||||
form.setValue(
|
||||
"requireDeviceApproval",
|
||||
checked
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={t(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
{t(
|
||||
"requireDeviceApprovalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
@@ -235,6 +127,5 @@ export default function CreateRoleForm({
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||
return (
|
||||
<CredenzaContent
|
||||
className={cn(
|
||||
"overflow-y-auto max-h-[100dvh] md:max-h-screen",
|
||||
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,44 +11,26 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import type { Role } from "@server/db";
|
||||
import type {
|
||||
CreateRoleBody,
|
||||
CreateRoleResponse,
|
||||
UpdateRoleBody,
|
||||
UpdateRoleResponse
|
||||
} from "@server/routers/role";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { RoleForm, type RoleFormValues } from "./RoleForm";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
type EditRoleFormProps = {
|
||||
role: Role;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSuccess?: (res: CreateRoleResponse) => void;
|
||||
onSuccess?: (res: UpdateRoleResponse) => void;
|
||||
};
|
||||
|
||||
export default function EditRoleForm({
|
||||
@@ -56,39 +38,44 @@ export default function EditRoleForm({
|
||||
role,
|
||||
setOpen,
|
||||
onSuccess
|
||||
}: CreateRoleFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
}: EditRoleFormProps) {
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string({ message: t("nameRequired") })
|
||||
.min(1)
|
||||
.max(32),
|
||||
description: z.string().max(255).optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
});
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: role.name,
|
||||
description: role.description ?? "",
|
||||
requireDeviceApproval: role.requireDeviceApproval ?? false
|
||||
}
|
||||
});
|
||||
|
||||
const [loading, startTransition] = useTransition();
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
async function onSubmit(values: RoleFormValues) {
|
||||
const payload: UpdateRoleBody = {
|
||||
requireDeviceApproval: values.requireDeviceApproval,
|
||||
allowSsh: values.allowSsh
|
||||
};
|
||||
if (!role.isAdmin) {
|
||||
payload.name = values.name;
|
||||
payload.description = values.description || undefined;
|
||||
}
|
||||
if (isPaidUser(tierMatrix.sshPam)) {
|
||||
payload.sshSudoMode = values.sshSudoMode;
|
||||
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
||||
payload.sshSudoCommands =
|
||||
values.sshSudoMode === "commands" &&
|
||||
values.sshSudoCommands?.trim()
|
||||
? values.sshSudoCommands
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (values.sshUnixGroups !== undefined) {
|
||||
payload.sshUnixGroups = values.sshUnixGroups
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
const res = await api
|
||||
.post<
|
||||
AxiosResponse<UpdateRoleResponse>
|
||||
>(`/role/${role.roleId}`, values satisfies UpdateRoleBody)
|
||||
.post<AxiosResponse<UpdateRoleResponse>>(
|
||||
`/role/${role.roleId}`,
|
||||
payload
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
@@ -106,24 +93,13 @@ export default function EditRoleForm({
|
||||
title: t("accessRoleUpdated"),
|
||||
description: t("accessRoleUpdatedDescription")
|
||||
});
|
||||
|
||||
if (open) {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
if (open) setOpen(false);
|
||||
onSuccess?.(res.data.data);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("accessRoleEdit")}</CredenzaTitle>
|
||||
@@ -132,101 +108,13 @@ export default function EditRoleForm({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) =>
|
||||
<RoleForm
|
||||
variant="edit"
|
||||
role={role}
|
||||
onSubmit={(values) =>
|
||||
startTransition(() => onSubmit(values))
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="create-role-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("accessRoleName")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("description")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.deviceApprovals}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireDeviceApproval"
|
||||
render={({ field }) => (
|
||||
<FormItem className="my-2">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.deviceApprovals
|
||||
)
|
||||
}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) => {
|
||||
if (
|
||||
checked !==
|
||||
"indeterminate"
|
||||
) {
|
||||
form.setValue(
|
||||
"requireDeviceApproval",
|
||||
checked
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={t(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
{t(
|
||||
"requireDeviceApprovalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
@@ -243,6 +131,5 @@ export default function EditRoleForm({
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
1328
src/components/InternalResourceForm.tsx
Normal file
1328
src/components/InternalResourceForm.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -75,7 +75,7 @@ export async function Layout({
|
||||
<div
|
||||
className={cn(
|
||||
"container mx-auto max-w-12xl mb-12",
|
||||
showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
|
||||
showHeader && "md:pt-14" // Add top padding only on desktop to account for fixed header
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -73,14 +73,14 @@ export function LayoutMobileMenu({
|
||||
{t("navbarDescription")}
|
||||
</SheetDescription>
|
||||
<div className="flex-1 overflow-y-auto relative">
|
||||
<div className="px-3">
|
||||
<div className="px-1">
|
||||
<OrgSelector
|
||||
orgId={orgId}
|
||||
orgs={orgs}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full border-b border-border" />
|
||||
<div className="px-3">
|
||||
<div className="px-3 pt-3">
|
||||
{!isAdminPage &&
|
||||
user.serverAdmin && (
|
||||
<div className="py-2">
|
||||
|
||||
@@ -18,7 +18,7 @@ import { approvalQueries } from "@app/lib/queries";
|
||||
import { build } from "@server/build";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { ExternalLink, Server } from "lucide-react";
|
||||
import { ArrowRight, ExternalLink, PanelRightOpen, Server } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
@@ -145,9 +145,19 @@ export function LayoutSidebar({
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto relative">
|
||||
<div className="px-2 pt-1">
|
||||
<div className="px-2 pt-3">
|
||||
<SidebarNav
|
||||
sections={navItems}
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
notificationCounts={notificationCounts}
|
||||
/>
|
||||
</div>
|
||||
{/* Fade gradient at bottom to indicate scrollable content */}
|
||||
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" />
|
||||
</div>
|
||||
|
||||
{!isAdminPage && user.serverAdmin && (
|
||||
<div className="py-2">
|
||||
<div className="shrink-0 px-2 pb-2">
|
||||
<Link
|
||||
href="/admin"
|
||||
className={cn(
|
||||
@@ -157,9 +167,7 @@ export function LayoutSidebar({
|
||||
: "px-3 py-1.5"
|
||||
)}
|
||||
title={
|
||||
isSidebarCollapsed
|
||||
? t("serverAdmin")
|
||||
: undefined
|
||||
isSidebarCollapsed ? t("serverAdmin") : undefined
|
||||
}
|
||||
>
|
||||
<span
|
||||
@@ -171,46 +179,66 @@ export function LayoutSidebar({
|
||||
<Server className="h-4 w-4" />
|
||||
</span>
|
||||
{!isSidebarCollapsed && (
|
||||
<span>{t("serverAdmin")}</span>
|
||||
<>
|
||||
<span className="flex-1">
|
||||
{t("serverAdmin")}
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 shrink-0 ml-auto opacity-70" />
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<SidebarNav
|
||||
sections={navItems}
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
notificationCounts={notificationCounts}
|
||||
/>
|
||||
</div>
|
||||
{/* Fade gradient at bottom to indicate scrollable content */}
|
||||
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="w-full border-t border-border" />
|
||||
{isSidebarCollapsed && (
|
||||
<div className="shrink-0 flex justify-center py-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsSidebarCollapsed(false);
|
||||
setHasManualToggle(true);
|
||||
setSidebarStateCookie(false);
|
||||
}}
|
||||
className="rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 transition-colors"
|
||||
aria-label={t("sidebarExpand")}
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<p>{t("sidebarExpand")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 pt-1 flex flex-col shrink-0">
|
||||
{canShowProductUpdates ? (
|
||||
<div className="mb-3">
|
||||
<div className="w-full border-t border-border mb-3" />
|
||||
|
||||
<div className="p-4 pt-0 mt-0 flex flex-col shrink-0">
|
||||
{canShowProductUpdates && (
|
||||
<div className="mb-3 empty:mb-0">
|
||||
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3"></div>
|
||||
)}
|
||||
|
||||
{build === "enterprise" && (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 empty:mb-0">
|
||||
<SidebarLicenseButton
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{build === "oss" && (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 empty:mb-0">
|
||||
<SupporterStatus isCollapsed={isSidebarCollapsed} />
|
||||
</div>
|
||||
)}
|
||||
{build === "saas" && (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 empty:mb-0">
|
||||
<SidebarSupportButton
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
/>
|
||||
@@ -226,19 +254,19 @@ export function LayoutSidebar({
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{link.href ? (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
<Link
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1"
|
||||
className="flex items-center justify-start gap-1"
|
||||
>
|
||||
{link.text}
|
||||
<ExternalLink size={12} />
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
{link.text}
|
||||
</div>
|
||||
)}
|
||||
@@ -247,12 +275,12 @@ export function LayoutSidebar({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
<Link
|
||||
href="https://github.com/fosrl/pangolin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1"
|
||||
className="flex items-center justify-start gap-1"
|
||||
>
|
||||
{build === "oss"
|
||||
? t("communityEdition")
|
||||
@@ -265,22 +293,22 @@ export function LayoutSidebar({
|
||||
{build === "enterprise" &&
|
||||
isUnlocked() &&
|
||||
licenseStatus?.tier === "personal" ? (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
{t("personalUseOnly")}
|
||||
</div>
|
||||
) : null}
|
||||
{build === "enterprise" && !isUnlocked() ? (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
{t("unlicensed")}
|
||||
</div>
|
||||
) : null}
|
||||
{env?.app?.version && (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-left">
|
||||
<Link
|
||||
href={`https://github.com/fosrl/pangolin/releases/tag/${env.app.version}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1"
|
||||
className="flex items-center justify-start gap-1"
|
||||
>
|
||||
v{env.app.version}
|
||||
<ExternalLink size={12} />
|
||||
|
||||
70
src/components/OptionSelect.tsx
Normal file
70
src/components/OptionSelect.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type OptionSelectOption<TValue extends string> = {
|
||||
value: TValue;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
type OptionSelectProps<TValue extends string> = {
|
||||
options: ReadonlyArray<OptionSelectOption<TValue>>;
|
||||
value: TValue;
|
||||
onChange: (value: TValue) => void;
|
||||
label?: string;
|
||||
/** Grid columns: 2, 3, 4, 5, etc. Default 5 on md+. */
|
||||
cols?: number;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function OptionSelect<TValue extends string>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
cols = 5,
|
||||
className,
|
||||
disabled = false
|
||||
}: OptionSelectProps<TValue>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<p className="font-bold mb-3">{label}</p>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-2",
|
||||
cols === 2 && "grid-cols-2",
|
||||
cols === 3 && "grid-cols-2 md:grid-cols-3",
|
||||
cols === 4 && "grid-cols-2 md:grid-cols-4",
|
||||
cols === 5 && "grid-cols-2 md:grid-cols-5",
|
||||
cols === 6 && "grid-cols-2 md:grid-cols-3 lg:grid-cols-6"
|
||||
)}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const isSelected = value === option.value;
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
variant={isSelected ? "squareOutlinePrimary" : "squareOutline"}
|
||||
className={cn(
|
||||
"flex-1 min-w-30 shadow-none",
|
||||
isSelected && "bg-primary/10"
|
||||
)}
|
||||
onClick={() => onChange(option.value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator
|
||||
CommandList
|
||||
} from "@app/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
@@ -25,6 +24,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { Check, ChevronsUpDown, Plus, Building2, Users } from "lucide-react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
@@ -71,7 +71,7 @@ export function OrgSelector({
|
||||
"cursor-pointer transition-colors",
|
||||
isCollapsed
|
||||
? "w-full h-16 flex items-center justify-center hover:bg-muted"
|
||||
: "w-full px-4 py-4 hover:bg-muted"
|
||||
: "w-full px-5 py-4 hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
@@ -93,68 +93,45 @@ export function OrgSelector({
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[320px] p-0" align="start">
|
||||
<Command className="rounded-lg">
|
||||
<PopoverContent
|
||||
className="w-[320px] p-0 ml-4 flex flex-col relative overflow-visible"
|
||||
align="start"
|
||||
sideOffset={12}
|
||||
>
|
||||
<Command className="rounded-lg border-0 flex-1 min-h-0">
|
||||
<CommandInput
|
||||
placeholder={t("searchPlaceholder")}
|
||||
className="border-0 focus:ring-0"
|
||||
className="border-0 focus:ring-0 h-9 rounded-b-none"
|
||||
/>
|
||||
<CommandEmpty className="py-6 text-center">
|
||||
<CommandList className="max-h-[280px]">
|
||||
<CommandEmpty className="py-4 text-center">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("orgNotFound2")}
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
|
||||
<>
|
||||
<CommandGroup
|
||||
heading={t("create")}
|
||||
className="py-2"
|
||||
>
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push("/setup");
|
||||
}}
|
||||
className="mx-2 rounded-md"
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
||||
<Plus className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{t("setupNewOrg")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("createNewOrgDescription")}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandSeparator className="my-2" />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading={t("orgs")} className="py-2">
|
||||
<CommandList>
|
||||
<CommandGroup className="p-1" heading={t("orgs")}>
|
||||
{sortedOrgs.map((org) => (
|
||||
<CommandItem
|
||||
key={org.orgId}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
const newPath = pathname.replace(
|
||||
const newPath = pathname.includes(
|
||||
"/settings/"
|
||||
)
|
||||
? pathname.replace(
|
||||
/^\/[^/]+/,
|
||||
`/${org.orgId}`
|
||||
);
|
||||
)
|
||||
: `/${org.orgId}`;
|
||||
router.push(newPath);
|
||||
}}
|
||||
className="mx-2 rounded-md"
|
||||
className="mx-1 rounded-md py-1.5 h-auto min-h-0"
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted mr-3">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-md bg-muted mr-2.5 flex-shrink-0">
|
||||
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<span className="font-medium truncate">
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-0.5">
|
||||
<span className="font-medium truncate text-sm">
|
||||
{org.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -173,7 +150,7 @@ export function OrgSelector({
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary",
|
||||
"h-4 w-4 text-primary flex-shrink-0",
|
||||
orgId === org.orgId
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
@@ -181,9 +158,25 @@ export function OrgSelector({
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
|
||||
<div className="p-2 border-t border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start h-8 font-normal text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
router.push("/setup");
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-2" />
|
||||
{t("setupNewOrg")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -12,34 +12,42 @@ import { useParams } from "next/navigation";
|
||||
|
||||
const TIER_ORDER: Tier[] = ["tier1", "tier2", "tier3", "enterprise"];
|
||||
|
||||
const TIER_TRANSLATION_KEYS: Record<Tier, "subscriptionTierTier1" | "subscriptionTierTier2" | "subscriptionTierTier3" | "subscriptionTierEnterprise"> = {
|
||||
const TIER_TRANSLATION_KEYS: Record<
|
||||
Tier,
|
||||
| "subscriptionTierTier1"
|
||||
| "subscriptionTierTier2"
|
||||
| "subscriptionTierTier3"
|
||||
| "subscriptionTierEnterprise"
|
||||
> = {
|
||||
tier1: "subscriptionTierTier1",
|
||||
tier2: "subscriptionTierTier2",
|
||||
tier3: "subscriptionTierTier3",
|
||||
enterprise: "subscriptionTierEnterprise"
|
||||
};
|
||||
|
||||
function getRequiredTier(tiers: Tier[]): Tier | null {
|
||||
function formatRequiredTiersList(
|
||||
tiers: Tier[],
|
||||
t: (key: (typeof TIER_TRANSLATION_KEYS)[Tier]) => string
|
||||
): string | null {
|
||||
if (tiers.length === 0) return null;
|
||||
let min: Tier | null = null;
|
||||
for (const tier of tiers) {
|
||||
const idx = TIER_ORDER.indexOf(tier);
|
||||
if (idx === -1) continue;
|
||||
if (min === null || TIER_ORDER.indexOf(min) > idx) {
|
||||
min = tier;
|
||||
}
|
||||
}
|
||||
return min;
|
||||
const sorted = [...tiers]
|
||||
.filter((tier) => TIER_ORDER.includes(tier))
|
||||
.sort((a, b) => TIER_ORDER.indexOf(a) - TIER_ORDER.indexOf(b));
|
||||
if (sorted.length === 0) return null;
|
||||
const names = sorted.map((tier) => t(TIER_TRANSLATION_KEYS[tier]));
|
||||
if (names.length === 1) return names[0];
|
||||
if (names.length === 2) return `${names[0]} or ${names[1]}`;
|
||||
return `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}`;
|
||||
}
|
||||
|
||||
const bannerClassName =
|
||||
"mb-6 border-purple-500/30 bg-linear-to-br from-purple-500/10 via-background to-background overflow-hidden";
|
||||
"mb-6 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden";
|
||||
const bannerContentClassName = "py-3 px-4";
|
||||
const bannerRowClassName =
|
||||
"flex items-center gap-2.5 text-sm text-muted-foreground";
|
||||
const bannerIconClassName = "size-4 shrink-0 text-purple-500";
|
||||
const bannerIconClassName = "size-4 shrink-0 text-black-500";
|
||||
const docsLinkClassName =
|
||||
"inline-flex items-center gap-1 font-medium text-purple-600 underline";
|
||||
"inline-flex items-center gap-1 font-medium text-black-600 underline";
|
||||
const PANGOLIN_CLOUD_SIGNUP_URL = "https://app.pangolin.net/auth/signup/";
|
||||
const ENTERPRISE_DOCS_URL =
|
||||
"https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
@@ -94,11 +102,17 @@ export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
const t = useTranslations();
|
||||
const params = useParams();
|
||||
const orgId = params?.orgId as string | undefined;
|
||||
const { hasSaasSubscription, hasEnterpriseLicense, isActive, subscriptionTier } = usePaidStatus();
|
||||
const {
|
||||
hasSaasSubscription,
|
||||
hasEnterpriseLicense,
|
||||
isActive,
|
||||
subscriptionTier
|
||||
} = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
const requiredTier = getRequiredTier(tiers);
|
||||
const requiredTierName = requiredTier ? t(TIER_TRANSLATION_KEYS[requiredTier]) : null;
|
||||
const billingHref = orgId ? `/${orgId}/settings/billing` : "https://pangolin.net/pricing";
|
||||
const requiredTiersLabel = formatRequiredTiersList(tiers, t);
|
||||
const billingHref = orgId
|
||||
? `/${orgId}/settings/billing`
|
||||
: "https://pangolin.net/pricing";
|
||||
const tierLinkRenderer = getTierLinkRenderer(billingHref);
|
||||
const pangolinCloudLinkRenderer = getPangolinCloudLinkRenderer();
|
||||
const enterpriseDocsLinkRenderer = getDocsLinkRenderer(ENTERPRISE_DOCS_URL);
|
||||
@@ -115,14 +129,14 @@ export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
{requiredTierName
|
||||
{requiredTiersLabel
|
||||
? isActive
|
||||
? t.rich("upgradeToTierToUse", {
|
||||
tier: requiredTierName,
|
||||
tier: requiredTiersLabel,
|
||||
tierLink: tierLinkRenderer
|
||||
})
|
||||
: t.rich("subscriptionRequiredTierToUse", {
|
||||
tier: requiredTierName,
|
||||
: t.rich("upgradeToTierToUse", {
|
||||
tier: requiredTiersLabel,
|
||||
tierLink: tierLinkRenderer
|
||||
})
|
||||
: isActive
|
||||
@@ -141,7 +155,8 @@ export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
{t.rich("licenseRequiredToUse", {
|
||||
enterpriseLicenseLink: enterpriseDocsLinkRenderer,
|
||||
enterpriseLicenseLink:
|
||||
enterpriseDocsLinkRenderer,
|
||||
pangolinCloudLink: pangolinCloudLinkRenderer
|
||||
})}
|
||||
</span>
|
||||
@@ -157,7 +172,8 @@ export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
{t.rich("ossEnterpriseEditionRequired", {
|
||||
enterpriseEditionLink: enterpriseDocsLinkRenderer,
|
||||
enterpriseEditionLink:
|
||||
enterpriseDocsLinkRenderer,
|
||||
pangolinCloudLink: pangolinCloudLinkRenderer
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function ProductUpdates({
|
||||
<div className="flex flex-col gap-1">
|
||||
<small
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground flex items-center gap-1 mt-2",
|
||||
"text-xs text-muted-foreground flex items-center gap-1 mt-2 empty:mt-0",
|
||||
showMoreUpdatesText
|
||||
? "animate-in fade-in duration-300"
|
||||
: "opacity-0"
|
||||
|
||||
468
src/components/RoleForm.tsx
Normal file
468
src/components/RoleForm.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
OptionSelect,
|
||||
type OptionSelectOption
|
||||
} from "@app/components/OptionSelect";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { Role } from "@server/db";
|
||||
|
||||
export const SSH_SUDO_MODE_VALUES = ["none", "full", "commands"] as const;
|
||||
export type SshSudoMode = (typeof SSH_SUDO_MODE_VALUES)[number];
|
||||
|
||||
function parseRoleJsonArray(value: string | null | undefined): string[] {
|
||||
if (value == null || value === "") return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function toSshSudoMode(value: string | null | undefined): SshSudoMode {
|
||||
if (value === "none" || value === "full" || value === "commands")
|
||||
return value;
|
||||
return "none";
|
||||
}
|
||||
|
||||
export type RoleFormValues = {
|
||||
name: string;
|
||||
description?: string;
|
||||
requireDeviceApproval?: boolean;
|
||||
allowSsh?: boolean;
|
||||
sshSudoMode: SshSudoMode;
|
||||
sshSudoCommands?: string;
|
||||
sshCreateHomeDir?: boolean;
|
||||
sshUnixGroups?: string;
|
||||
};
|
||||
|
||||
type RoleFormProps = {
|
||||
variant: "create" | "edit";
|
||||
role?: Role;
|
||||
onSubmit: (values: RoleFormValues) => void | Promise<void>;
|
||||
formId?: string;
|
||||
};
|
||||
|
||||
export function RoleForm({
|
||||
variant,
|
||||
role,
|
||||
onSubmit,
|
||||
formId = "create-role-form"
|
||||
}: RoleFormProps) {
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string({ message: t("nameRequired") })
|
||||
.min(1)
|
||||
.max(32),
|
||||
description: z.string().max(255).optional(),
|
||||
requireDeviceApproval: z.boolean().optional(),
|
||||
allowSsh: z.boolean().optional(),
|
||||
sshSudoMode: z.enum(SSH_SUDO_MODE_VALUES),
|
||||
sshSudoCommands: z.string().optional(),
|
||||
sshCreateHomeDir: z.boolean().optional(),
|
||||
sshUnixGroups: z.string().optional()
|
||||
});
|
||||
|
||||
const defaultValues: RoleFormValues = role
|
||||
? {
|
||||
name: role.name,
|
||||
description: role.description ?? "",
|
||||
requireDeviceApproval: role.requireDeviceApproval ?? false,
|
||||
allowSsh:
|
||||
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
|
||||
sshSudoMode: toSshSudoMode(role.sshSudoMode),
|
||||
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
|
||||
", "
|
||||
),
|
||||
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
|
||||
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
description: "",
|
||||
requireDeviceApproval: false,
|
||||
allowSsh: false,
|
||||
sshSudoMode: "none",
|
||||
sshSudoCommands: "",
|
||||
sshCreateHomeDir: true,
|
||||
sshUnixGroups: ""
|
||||
};
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (variant === "edit" && role) {
|
||||
form.reset({
|
||||
name: role.name,
|
||||
description: role.description ?? "",
|
||||
requireDeviceApproval: role.requireDeviceApproval ?? false,
|
||||
allowSsh:
|
||||
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
|
||||
sshSudoMode: toSshSudoMode(role.sshSudoMode),
|
||||
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
|
||||
", "
|
||||
),
|
||||
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
|
||||
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
|
||||
});
|
||||
}
|
||||
}, [variant, role, form]);
|
||||
|
||||
const sshDisabled = !isPaidUser(tierMatrix.sshPam);
|
||||
const sshSudoMode = form.watch("sshSudoMode");
|
||||
const isAdminRole = variant === "edit" && role?.isAdmin === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (sshDisabled) {
|
||||
form.setValue("allowSsh", false);
|
||||
}
|
||||
}, [sshDisabled, form]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) => onSubmit(values))}
|
||||
className="space-y-4"
|
||||
id={formId}
|
||||
>
|
||||
{env.flags.disableEnterpriseFeatures ? (
|
||||
<div className="space-y-4 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("accessRoleName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isAdminRole}
|
||||
readOnly={isAdminRole}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("description")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isAdminRole}
|
||||
readOnly={isAdminRole}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<HorizontalTabs
|
||||
clientSide={true}
|
||||
defaultTab={0}
|
||||
items={[
|
||||
{ title: t("general"), href: "#" },
|
||||
...(env.flags.disableEnterpriseFeatures
|
||||
? []
|
||||
: [{ title: t("sshAccess"), href: "#" }])
|
||||
]}
|
||||
>
|
||||
{/* General tab */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("accessRoleName")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isAdminRole}
|
||||
readOnly={isAdminRole}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("description")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isAdminRole}
|
||||
readOnly={isAdminRole}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.deviceApprovals}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireDeviceApproval"
|
||||
render={({ field }) => (
|
||||
<FormItem className="my-2">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.deviceApprovals
|
||||
)
|
||||
}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (
|
||||
checked !==
|
||||
"indeterminate"
|
||||
) {
|
||||
form.setValue(
|
||||
"requireDeviceApproval",
|
||||
checked
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={t(
|
||||
"requireDeviceApproval"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"requireDeviceApprovalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* SSH tab - hidden when enterprise features are disabled */}
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<div className="space-y-4 mt-4">
|
||||
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowSsh"
|
||||
render={({ field }) => {
|
||||
const allowSshOptions: OptionSelectOption<"allow" | "disallow">[] = [
|
||||
{
|
||||
value: "allow",
|
||||
label: t("roleAllowSshAllow")
|
||||
},
|
||||
{
|
||||
value: "disallow",
|
||||
label: t("roleAllowSshDisallow")
|
||||
}
|
||||
];
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("roleAllowSsh")}
|
||||
</FormLabel>
|
||||
<OptionSelect<"allow" | "disallow">
|
||||
options={allowSshOptions}
|
||||
value={
|
||||
sshDisabled
|
||||
? "disallow"
|
||||
: field.value
|
||||
? "allow"
|
||||
: "disallow"
|
||||
}
|
||||
onChange={(v) => {
|
||||
if (sshDisabled) return;
|
||||
field.onChange(v === "allow");
|
||||
}}
|
||||
cols={2}
|
||||
disabled={sshDisabled}
|
||||
/>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"roleAllowSshDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshSudoMode"
|
||||
render={({ field }) => {
|
||||
const sudoOptions: OptionSelectOption<SshSudoMode>[] =
|
||||
[
|
||||
{
|
||||
value: "none",
|
||||
label: t("sshSudoModeNone")
|
||||
},
|
||||
{
|
||||
value: "full",
|
||||
label: t("sshSudoModeFull")
|
||||
},
|
||||
{
|
||||
value: "commands",
|
||||
label: t(
|
||||
"sshSudoModeCommands"
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshSudoMode")}
|
||||
</FormLabel>
|
||||
<OptionSelect<SshSudoMode>
|
||||
options={sudoOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
cols={3}
|
||||
disabled={sshDisabled}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{sshSudoMode === "commands" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshSudoCommands"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshSudoCommands")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={sshDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"sshSudoCommandsDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshUnixGroups"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshUnixGroups")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={sshDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("sshUnixGroupsDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshCreateHomeDir"
|
||||
render={({ field }) => (
|
||||
<FormItem className="my-2">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
"sshCreateHomeDir"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) => {
|
||||
if (
|
||||
checked !==
|
||||
"indeterminate"
|
||||
) {
|
||||
form.setValue(
|
||||
"sshCreateHomeDir",
|
||||
checked
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={t(
|
||||
"sshCreateHomeDir"
|
||||
)}
|
||||
disabled={sshDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</HorizontalTabs>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -103,14 +103,15 @@ export default function UsersTable({ roles }: RolesTableProps) {
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const roleRow = row.original;
|
||||
const isAdmin = roleRow.isAdmin;
|
||||
return (
|
||||
!roleRow.isAdmin && (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isAdmin || false}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
@@ -120,6 +121,7 @@ export default function UsersTable({ roles }: RolesTableProps) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
disabled={isAdmin || false}
|
||||
onClick={() => {
|
||||
setRoleToRemove(roleRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -141,7 +143,6 @@ export default function UsersTable({ roles }: RolesTableProps) {
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ function CollapsibleNavItem({
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center w-full rounded-md transition-colors",
|
||||
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
|
||||
"px-3 py-1.5",
|
||||
isActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||
@@ -128,7 +128,7 @@ function CollapsibleNavItem({
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
|
||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center text-muted-foreground">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
@@ -167,10 +167,17 @@ function CollapsibleNavItem({
|
||||
</div>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CollapsibleContent forceMount>
|
||||
<div
|
||||
className={cn(
|
||||
"border-l ml-3 pl-3 mt-0 space-y-0",
|
||||
"grid overflow-hidden transition-[grid-template-rows] duration-200 ease-in-out",
|
||||
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
||||
)}
|
||||
>
|
||||
<div className="min-h-0">
|
||||
<div
|
||||
className={cn(
|
||||
"border-l ml-[22px] pl-[9px] mt-0 space-y-0",
|
||||
"border-border"
|
||||
)}
|
||||
>
|
||||
@@ -178,11 +185,174 @@ function CollapsibleNavItem({
|
||||
renderNavItem(childItem, level + 1)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
type CollapsedNavItemWithPopoverProps = {
|
||||
item: SidebarNavItem;
|
||||
tooltipText: string;
|
||||
isActive: boolean;
|
||||
isChildActive: boolean;
|
||||
isDisabled: boolean;
|
||||
hydrateHref: (val?: string) => string | undefined;
|
||||
pathname: string;
|
||||
build: string;
|
||||
isUnlocked: () => boolean;
|
||||
disabled: boolean;
|
||||
t: (key: string) => string;
|
||||
onItemClick?: () => void;
|
||||
};
|
||||
|
||||
const TOOLTIP_SUPPRESS_MS = 400;
|
||||
|
||||
function CollapsedNavItemWithPopover({
|
||||
item,
|
||||
tooltipText,
|
||||
isActive,
|
||||
isChildActive,
|
||||
isDisabled,
|
||||
hydrateHref,
|
||||
pathname,
|
||||
build,
|
||||
isUnlocked,
|
||||
disabled,
|
||||
t,
|
||||
onItemClick
|
||||
}: CollapsedNavItemWithPopoverProps) {
|
||||
const [popoverOpen, setPopoverOpen] = React.useState(false);
|
||||
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
||||
const suppressTooltipRef = React.useRef(false);
|
||||
|
||||
const handlePopoverOpenChange = React.useCallback((open: boolean) => {
|
||||
setPopoverOpen(open);
|
||||
if (!open) {
|
||||
setTooltipOpen(false);
|
||||
suppressTooltipRef.current = true;
|
||||
window.setTimeout(() => {
|
||||
suppressTooltipRef.current = false;
|
||||
}, TOOLTIP_SUPPRESS_MS);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTooltipOpenChange = React.useCallback((open: boolean) => {
|
||||
if (open && suppressTooltipRef.current) return;
|
||||
setTooltipOpen(open);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip open={tooltipOpen} onOpenChange={handleTooltipOpenChange}>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onOpenChange={handlePopoverOpenChange}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full",
|
||||
isActive || isChildActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||
isDisabled &&
|
||||
"cursor-not-allowed opacity-60"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
</PopoverTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<p>{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="w-56 p-1"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{item.items!.map((childItem) => {
|
||||
const childHydratedHref = hydrateHref(
|
||||
childItem.href
|
||||
);
|
||||
const childIsActive = childHydratedHref
|
||||
? pathname.startsWith(childHydratedHref)
|
||||
: false;
|
||||
const childIsEE =
|
||||
build === "enterprise" &&
|
||||
childItem.showEE &&
|
||||
!isUnlocked();
|
||||
const childIsDisabled = disabled || childIsEE;
|
||||
|
||||
if (!childHydratedHref) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={childItem.title}
|
||||
href={
|
||||
childIsDisabled
|
||||
? "#"
|
||||
: childHydratedHref
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors px-3 py-1.5 text-sm",
|
||||
childIsActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground",
|
||||
childIsDisabled &&
|
||||
"cursor-not-allowed opacity-60"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (childIsDisabled) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
handlePopoverOpenChange(false);
|
||||
onItemClick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate">
|
||||
{t(childItem.title)}
|
||||
</span>
|
||||
{childItem.isBeta && (
|
||||
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||
{t("beta")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{build === "enterprise" &&
|
||||
childItem.showEE &&
|
||||
!isUnlocked() && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="flex-shrink-0 ml-2"
|
||||
>
|
||||
{t("licenseBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarNav({
|
||||
className,
|
||||
sections,
|
||||
@@ -278,11 +448,7 @@ export function SidebarNav({
|
||||
href={isDisabled ? "#" : hydratedHref}
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors relative",
|
||||
isCollapsed
|
||||
? "px-2 py-2 justify-center"
|
||||
: level === 0
|
||||
? "px-3 py-1.5"
|
||||
: "px-3 py-1",
|
||||
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
|
||||
isActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||
@@ -298,10 +464,13 @@ export function SidebarNav({
|
||||
tabIndex={isDisabled ? -1 : undefined}
|
||||
aria-disabled={isDisabled}
|
||||
>
|
||||
{item.icon && (
|
||||
{item.icon && level === 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"flex-shrink-0 w-5 h-5 flex items-center justify-center",
|
||||
isCollapsed
|
||||
? "text-muted-foreground"
|
||||
: "text-muted-foreground",
|
||||
!isCollapsed && "mr-3"
|
||||
)}
|
||||
>
|
||||
@@ -355,13 +524,13 @@ export function SidebarNav({
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors",
|
||||
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
|
||||
"px-3 py-1.5",
|
||||
"text-muted-foreground",
|
||||
isDisabled && "cursor-not-allowed opacity-60"
|
||||
)}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
|
||||
{item.icon && level === 0 && (
|
||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center text-muted-foreground">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
@@ -401,120 +570,21 @@ export function SidebarNav({
|
||||
// If item has nested items, show both tooltip and popover
|
||||
if (hasNestedItems) {
|
||||
return (
|
||||
<TooltipProvider key={item.title}>
|
||||
<Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full",
|
||||
isActive || isChildActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
|
||||
isDisabled &&
|
||||
"cursor-not-allowed opacity-60"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
</PopoverTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<p>{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="w-56 p-1"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{item.items!.map((childItem) => {
|
||||
const childHydratedHref =
|
||||
hydrateHref(childItem.href);
|
||||
const childIsActive =
|
||||
childHydratedHref
|
||||
? pathname.startsWith(
|
||||
childHydratedHref
|
||||
)
|
||||
: false;
|
||||
const childIsEE =
|
||||
build === "enterprise" &&
|
||||
childItem.showEE &&
|
||||
!isUnlocked();
|
||||
const childIsDisabled =
|
||||
disabled || childIsEE;
|
||||
|
||||
if (!childHydratedHref) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={childItem.title}
|
||||
href={
|
||||
childIsDisabled
|
||||
? "#"
|
||||
: childHydratedHref
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center rounded-md transition-colors px-3 py-1.5 text-sm",
|
||||
childIsActive
|
||||
? "bg-secondary font-medium"
|
||||
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground",
|
||||
childIsDisabled &&
|
||||
"cursor-not-allowed opacity-60"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (childIsDisabled) {
|
||||
e.preventDefault();
|
||||
} else if (
|
||||
onItemClick
|
||||
) {
|
||||
onItemClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{childItem.icon && (
|
||||
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
|
||||
{childItem.icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate">
|
||||
{t(childItem.title)}
|
||||
</span>
|
||||
{childItem.isBeta && (
|
||||
<span className="uppercase font-mono text-yellow-600 dark:text-yellow-800 font-black text-xs">
|
||||
{t("beta")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{build === "enterprise" &&
|
||||
childItem.showEE &&
|
||||
!isUnlocked() && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="flex-shrink-0 ml-2"
|
||||
>
|
||||
{t(
|
||||
"licenseBadge"
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<CollapsedNavItemWithPopover
|
||||
key={item.title}
|
||||
item={item}
|
||||
tooltipText={tooltipText}
|
||||
isActive={isActive}
|
||||
isChildActive={isChildActive}
|
||||
isDisabled={!!isDisabled}
|
||||
hydrateHref={hydrateHref}
|
||||
pathname={pathname}
|
||||
build={build}
|
||||
isUnlocked={isUnlocked}
|
||||
disabled={disabled ?? false}
|
||||
t={t}
|
||||
onItemClick={onItemClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -549,7 +619,7 @@ export function SidebarNav({
|
||||
className={cn(sectionIndex > 0 && "mt-4")}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<div className="px-3 py-2 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider">
|
||||
<div className="px-3 py-2 text-xs font-medium text-foreground uppercase tracking-wider">
|
||||
{t(`${section.heading}`)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface StrategyOption<TValue extends string> {
|
||||
|
||||
interface StrategySelectProps<TValue extends string> {
|
||||
options: ReadonlyArray<StrategyOption<TValue>>;
|
||||
value?: TValue | null;
|
||||
defaultValue?: TValue;
|
||||
onChange?: (value: TValue) => void;
|
||||
cols?: number;
|
||||
@@ -21,18 +22,21 @@ interface StrategySelectProps<TValue extends string> {
|
||||
|
||||
export function StrategySelect<TValue extends string>({
|
||||
options,
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [selected, setSelected] = useState<TValue | undefined>(defaultValue);
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<TValue | undefined>(defaultValue);
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const selected = isControlled ? (controlledValue ?? undefined) : uncontrolledSelected;
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue={defaultValue}
|
||||
value={selected ?? ""}
|
||||
onValueChange={(value: string) => {
|
||||
const typedValue = value as TValue;
|
||||
setSelected(typedValue);
|
||||
if (!isControlled) setUncontrolledSelected(typedValue);
|
||||
onChange?.(typedValue);
|
||||
}}
|
||||
className={`grid md:grid-cols-${cols ? cols : 1} gap-4`}
|
||||
|
||||
@@ -155,16 +155,19 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
const isCurrentUser =
|
||||
`${userRow.username}-${userRow.idpId}` ===
|
||||
`${user?.username}-${user?.idpId}`;
|
||||
const isDisabled = userRow.isOwner || isCurrentUser;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div>
|
||||
{!userRow.isOwner && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
@@ -176,21 +179,22 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
className="block w-full"
|
||||
aria-disabled={isDisabled}
|
||||
onClick={(e) =>
|
||||
isDisabled && e.preventDefault()
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("accessUsersManage")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{`${userRow.username}-${userRow.idpId}` !==
|
||||
`${user?.username}-${user?.idpId}` && (
|
||||
{!isDisabled && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(
|
||||
true
|
||||
);
|
||||
setSelectedUser(
|
||||
userRow
|
||||
);
|
||||
setIsDeleteModalOpen(true);
|
||||
setSelectedUser(userRow);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
@@ -200,17 +204,23 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!userRow.isOwner && (
|
||||
{isDisabled ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled={userRow.isOwner}
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SettingsSectionTitle
|
||||
} from "./Settings";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { Button } from "./ui/button";
|
||||
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
|
||||
import { useState } from "react";
|
||||
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
|
||||
import { Terminal } from "lucide-react";
|
||||
@@ -138,6 +138,14 @@ WantedBy=default.target`
|
||||
|
||||
const commands = commandList[platform][architecture];
|
||||
|
||||
const platformOptions: OptionSelectOption<Platform>[] = PLATFORMS.map(
|
||||
(os) => ({
|
||||
value: os,
|
||||
label: getPlatformName(os),
|
||||
icon: getPlatformIcon(os)
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
@@ -149,53 +157,33 @@ WantedBy=default.target`
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{PLATFORMS.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
<OptionSelect<Platform>
|
||||
label={t("operatingSystem")}
|
||||
options={platformOptions}
|
||||
value={platform}
|
||||
onChange={(os) => {
|
||||
setPlatform(os);
|
||||
const architectures = getArchitectures(os);
|
||||
setArchitecture(architectures[0]);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
cols={5}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(platform)
|
||||
<OptionSelect<string>
|
||||
label={
|
||||
["docker", "podman"].includes(platform)
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures(platform).map((arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture === arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
: t("architecture")
|
||||
}
|
||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => setArchitecture(arch)}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
options={getArchitectures(platform).map((arch) => ({
|
||||
value: arch,
|
||||
label: arch
|
||||
}))}
|
||||
value={architecture}
|
||||
onChange={setArchitecture}
|
||||
cols={5}
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">
|
||||
@@ -250,7 +238,6 @@ WantedBy=default.target`
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "./Settings";
|
||||
import { Button } from "./ui/button";
|
||||
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
|
||||
|
||||
export type CommandItem = string | { title: string; command: string };
|
||||
|
||||
@@ -88,6 +88,15 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
|
||||
};
|
||||
|
||||
const commands = commandList[platform][architecture];
|
||||
|
||||
const platformOptions: OptionSelectOption<Platform>[] = PLATFORMS.map(
|
||||
(os) => ({
|
||||
value: os,
|
||||
label: getPlatformName(os),
|
||||
icon: getPlatformIcon(os)
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
@@ -99,53 +108,34 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{PLATFORMS.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
<OptionSelect<Platform>
|
||||
label={t("operatingSystem")}
|
||||
options={platformOptions}
|
||||
value={platform}
|
||||
onChange={(os) => {
|
||||
setPlatform(os);
|
||||
const architectures = getArchitectures(os);
|
||||
setArchitecture(architectures[0]);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
cols={5}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(platform)
|
||||
<OptionSelect<string>
|
||||
label={
|
||||
platform === "docker"
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures(platform).map((arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture === arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
: t("architecture")
|
||||
}
|
||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => setArchitecture(arch)}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
options={getArchitectures(platform).map((arch) => ({
|
||||
value: arch,
|
||||
label: arch
|
||||
}))}
|
||||
value={architecture}
|
||||
onChange={setArchitecture}
|
||||
cols={5}
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">{t("commands")}</p>
|
||||
<div className="mt-2 space-y-3">
|
||||
@@ -175,7 +165,6 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string) => {
|
||||
try {
|
||||
const subRes = await getCachedSubscription(orgId);
|
||||
subscribed =
|
||||
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") &&
|
||||
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3" || subRes.data.data.tier == "enterprise") &&
|
||||
subRes.data.data.active;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ export function SubscriptionStatusProvider({
|
||||
if (
|
||||
subscription.type == "tier1" ||
|
||||
subscription.type == "tier2" ||
|
||||
subscription.type == "tier3"
|
||||
subscription.type == "tier3" ||
|
||||
subscription.type == "enterprise"
|
||||
) {
|
||||
return {
|
||||
tier: subscription.type,
|
||||
@@ -61,7 +62,7 @@ export function SubscriptionStatusProvider({
|
||||
const isSubscribed = () => {
|
||||
const { tier, active } = getTier();
|
||||
return (
|
||||
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
|
||||
(tier == "tier1" || tier == "tier2" || tier == "tier3" || tier == "enterprise") &&
|
||||
active
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user