From 9d7764f874fbc9e406bbff352bae3b128901463a Mon Sep 17 00:00:00 2001 From: Abdulhafiz Davlatov Date: Sun, 6 Sep 2026 22:21:52 +0500 Subject: [PATCH 1/5] Add Uzbek locale and fix user permissions Introduces full Uzbek (uz-UZ) localization support across the frontend, including language registration and HelpDoc content. Fixes user permission update flow by normalizing user ID handling, avoiding writing `id` into permission payloads, and tightening backend/frontend behavior for admin permissions visibility. Also adds a local `docker-compose.yml` setup and ignores runtime data/certificate SQLite artifacts in `.gitignore`. --- .gitignore | 6 + backend/internal/user.js | 9 +- backend/routes/users.js | 2 +- docker-compose.yml | 24 + frontend/check-locales.cjs | 1 + frontend/src/locale/IntlProvider.tsx | 2 + frontend/src/locale/src/HelpDoc/index.ts | 3 +- .../src/locale/src/HelpDoc/uz/AccessLists.md | 7 + .../src/locale/src/HelpDoc/uz/Certificates.md | 21 + .../src/locale/src/HelpDoc/uz/DeadHosts.md | 7 + .../src/locale/src/HelpDoc/uz/ProxyHosts.md | 7 + .../locale/src/HelpDoc/uz/RedirectionHosts.md | 5 + frontend/src/locale/src/HelpDoc/uz/Streams.md | 5 + frontend/src/locale/src/HelpDoc/uz/index.ts | 6 + frontend/src/locale/src/lang-list.json | 3 + frontend/src/locale/src/uz.json | 785 ++++++++++++++++++ frontend/src/modals/PermissionsModal.tsx | 6 +- frontend/src/modals/ProxyHostModal.tsx | 2 +- frontend/src/pages/Users/Table.tsx | 42 + 19 files changed, 935 insertions(+), 8 deletions(-) create mode 100644 docker-compose.yml create mode 100644 frontend/src/locale/src/HelpDoc/uz/AccessLists.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/Certificates.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/DeadHosts.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/ProxyHosts.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/RedirectionHosts.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/Streams.md create mode 100644 frontend/src/locale/src/HelpDoc/uz/index.ts create mode 100644 frontend/src/locale/src/uz.json diff --git a/.gitignore b/.gitignore index a83a2110e0..7d6a1b1bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ docker/dev/dnsrouter-config.json.tmp docker/dev/resolv.conf .claude +# Runtime data & certificates +data/ +letsencrypt/ +*.sqlite +*.sqlite-journal + diff --git a/backend/internal/user.js b/backend/internal/user.js index d4080dd78f..92c12c0cce 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -39,7 +39,7 @@ const internalUser = { let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); if (auth) { - user = await authModel.query().insert({ + await authModel.query().insert({ user_id: user.id, type: auth.type, secret: auth.secret, @@ -434,7 +434,7 @@ const internalUser = { return internalUser.get(access, { id: data.id }); }) .then((user) => { - if (user.id !== data.id) { + if (Number(user.id) !== Number(data.id)) { // Sanity check that something crazy hasn't happened throw new errs.InternalValidationError( `User could not be updated, IDs do not match: ${user.id} !== ${data.id}`, @@ -444,6 +444,7 @@ const internalUser = { return user; }) .then((user) => { + const permData = _.omit(data, ["id"]); // Get perms row, patch if it exists return userPermissionModel .query() @@ -455,10 +456,10 @@ const internalUser = { return userPermissionModel .query() .where("user_id", user.id) - .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data)); + .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, permData)); } // insert - return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data)); + return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, permData)); }) .then((permissions) => { // Add to Audit Log diff --git a/backend/routes/users.js b/backend/routes/users.js index 3f972223be..2cdb4101cd 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -260,7 +260,7 @@ router .put(async (req, res, next) => { try { const payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body); - payload.id = req.params.user_id; + payload.id = Number.parseInt(req.params.user_id, 10); const result = await internalUser.setPermissions(res.locals.access, payload); res.status(200).send(result); } catch (err) { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..11dc8dd08f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + container_name: nginx-proxy-manager + restart: unless-stopped + ports: + # Public HTTP Port: + - '80:80' + # Admin Web Port: + - '81:81' + # Public HTTPS Port: + - '443:443' + environment: + # Default admin user credentials: + INITIAL_ADMIN_EMAIL: "admin@example.com" + INITIAL_ADMIN_PASSWORD: "changeme" + # Uncomment if IPv6 is not supported on your host: + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + - ./frontend/dist:/app/frontend:ro + - ./backend/internal/user.js:/app/internal/user.js:ro + - ./backend/routes/users.js:/app/routes/users.js:ro diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs index 1ae7546415..e827546c69 100755 --- a/frontend/check-locales.cjs +++ b/frontend/check-locales.cjs @@ -30,6 +30,7 @@ const allLocales = [ ["hu", "hu-HU"], ["no", "no-NO"], ["uk", "uk-UA"], + ["uz", "uz-UZ"], ]; const ignoreUnused = [/^.*$/]; diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx index 6150dd244e..b9bf64802c 100755 --- a/frontend/src/locale/IntlProvider.tsx +++ b/frontend/src/locale/IntlProvider.tsx @@ -22,6 +22,7 @@ import langTr from "./lang/tr.json"; import langHu from "./lang/hu.json"; import langNo from "./lang/no.json"; import langUk from "./lang/uk.json"; +import langUz from "./lang/uz.json"; import langList from "./lang/lang-list.json"; // first item of each array should be the language code, @@ -51,6 +52,7 @@ const localeOptions = [ ["hu", "hu-HU", langHu], ["no", "no-NO", langNo], ["uk", "uk-UA", langUk], + ["uz", "uz-UZ", langUz], ]; const loadMessages = (locale?: string): typeof langList & typeof langEn => { diff --git a/frontend/src/locale/src/HelpDoc/index.ts b/frontend/src/locale/src/HelpDoc/index.ts index 1fdd52def9..39aa04b42d 100644 --- a/frontend/src/locale/src/HelpDoc/index.ts +++ b/frontend/src/locale/src/HelpDoc/index.ts @@ -20,8 +20,9 @@ import * as zh from "./zh/index"; import * as tr from "./tr/index"; import * as hu from "./hu/index"; import * as uk from "./uk/index"; +import * as uz from "./uz/index"; -const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu, uk }; +const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu, uk, uz }; const fallbackLang = "en"; diff --git a/frontend/src/locale/src/HelpDoc/uz/AccessLists.md b/frontend/src/locale/src/HelpDoc/uz/AccessLists.md new file mode 100644 index 0000000000..277d3acbeb --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/AccessLists.md @@ -0,0 +1,7 @@ +## Kirish ro'yxati (Access List) nima? + +Kirish ro'yxatlari ma'lum mijoz IP manzillarining qora yoki oq ro'yxatini hamda Basic HTTP Authentication orqali Proxy Hostlar uchun autentifikatsiyani ta'minlaydi. + +Siz bitta Kirish ro'yxati uchun bir nechta mijoz qoidalarini, foydalanuvchi nomlari va parollarini sozlashingiz va keyin uni bir yoki bir nechta _Proxy Hostlar_ga qo'llashingiz mumkin. + +Bu, ayniqsa, o'zida autentifikatsiya mexanizmlari bo'lmagan xizmatlar yoki noma'lum mijozlardan himoyalanishni xohlaganingizda juda foydali. diff --git a/frontend/src/locale/src/HelpDoc/uz/Certificates.md b/frontend/src/locale/src/HelpDoc/uz/Certificates.md new file mode 100644 index 0000000000..7ce34dc02f --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/Certificates.md @@ -0,0 +1,21 @@ +## Sertifikatlar bo'yicha yordam + +### HTTP sertifikati + +HTTP orqali tasdiqlangan sertifikat shuni anglatadiki, Let's Encrypt serverlari sizning domenlaringizga HTTP (HTTPS emas!) orqali ulanishga harakat qiladi va muvaffaqiyatli bo'lsa, sertifikat beradi. + +Ushbu usul uchun sizning domen(lar)ingiz uchun HTTP orqali ochiq bo'lgan va ushbu Nginx o'rnatmasiga yo'naltirilgan _Proxy Host_ yaratilgan bo'lishi kerak. Sertifikat berilgandan so'ng, siz ushbu sertifikatdan HTTPS ulanishlari uchun ham foydalanish uchun _Proxy Host_ni o'zgartirishingiz mumkin. Biroq, sertifikat yangilanishi uchun _Proxy Host_ hali ham HTTP kirish uchun sozlangan bo'lishi kerak. + +Ushbu jarayon umumiy (wildcard) domenlarni _qo'llab-quvvatlamaydi_. + +### DNS sertifikati + +DNS orqali tasdiqlangan sertifikat sizdan DNS provayder plaginidan foydalanishni talab qiladi. Ushbu DNS provayderi domeningizda vaqtinchalik yozuvlar yaratish uchun ishlatiladi va keyin Let's Encrypt sizning egalik huquqingizga ishonch hosil qilish uchun ushbu yozuvlarni tekshiradi va muvaffaqiyatli bo'lsa, sertifikat beradi. + +Ushbu turdagi sertifikatni so'rashdan oldin _Proxy Host_ yaratishingiz shart emas. Shuningdek, _Proxy Host_ingiz HTTP kirish uchun sozlangan bo'lishi ham shart emas. + +Ushbu jarayon umumiy (wildcard) domenlarni _qo'llab-quvvatlaydi_. + +### Maxsus sertifikat (Custom Certificate) + +O'zingizning sertifikat markazingiz (Certificate Authority) tomonidan taqdim etilgan shaxsiy SSL sertifikatingizni yuklash uchun ushbu parametrdan foydalaning. diff --git a/frontend/src/locale/src/HelpDoc/uz/DeadHosts.md b/frontend/src/locale/src/HelpDoc/uz/DeadHosts.md new file mode 100644 index 0000000000..bee435b281 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/DeadHosts.md @@ -0,0 +1,7 @@ +## 404 Host nima? + +404 Host — bu shunchaki 404 ("Sahifa topilmadi") sahifasini ko'rsatadigan host sozlamasidir. + +Bu sizning domeningiz qidiruv tizimlarida ro'yxatga olingan bo'lsa va siz chiroyliroq xato sahifasini taqdim etishni yoki qidiruv indeksatorlariga ushbu domen sahifalari endi mavjud emasligini bildirishni xohlaganingizda foydali bo'ladi. + +Bunday hostga ega bo'lishning yana bir afzalligi — unga kelgan so'rovlar jurnallarini (log) kuzatish va havolalarni ko'rishdir. diff --git a/frontend/src/locale/src/HelpDoc/uz/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/uz/ProxyHosts.md new file mode 100644 index 0000000000..bcea93143c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/ProxyHosts.md @@ -0,0 +1,7 @@ +## Proxy Host nima? + +Proxy Host — bu siz yo'naltirmoqchi (forward) bo'lgan veb-xizmat uchun kiruvchi ulanish nuqtasidir. + +U o'zida SSL qo'llab-quvvatlovi bo'lmagan xizmatlaringiz uchun ixtiyoriy SSL terminatsiyasini taqdim etadi. + +Proxy Hostlar Nginx Proxy Manager-ning eng keng tarqalgan foydalanish holatidir. diff --git a/frontend/src/locale/src/HelpDoc/uz/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/uz/RedirectionHosts.md new file mode 100644 index 0000000000..a364a98738 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Qayta yo'naltirish hosti (Redirection Host) nima? + +Qayta yo'naltirish hosti kiruvchi domendan so'rovlarni qabul qilib, foydalanuvchini boshqa domenga yo'naltiradi. + +Bu turdagi hostdan foydalanishning eng asosiy sababi — veb-saytingiz domenini o'zgartirganingizda qidiruv tizimlari yoki tashqi havolalar hali ham eski domenga yo'naltirib turgan holatlardir. diff --git a/frontend/src/locale/src/HelpDoc/uz/Streams.md b/frontend/src/locale/src/HelpDoc/uz/Streams.md new file mode 100644 index 0000000000..acb70e6202 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/Streams.md @@ -0,0 +1,5 @@ +## Oqim (Stream) nima? + +Nginx uchun nisbatan yangi xususiyat bo'lgan Stream (oqim) TCP/UDP trafigini to'g'ridan-to'g'ri tarmoqdagi boshqa kompyuterga yo'naltirish uchun xizmat qiladi. + +Agar siz o'yin serverlari, FTP yoki SSH serverlarini boshqarayotgan bo'lsangiz, bu juda qulay bo'lishi mumkin. diff --git a/frontend/src/locale/src/HelpDoc/uz/index.ts b/frontend/src/locale/src/HelpDoc/uz/index.ts new file mode 100644 index 0000000000..155926983c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/uz/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/lang-list.json b/frontend/src/locale/src/lang-list.json index c6369821f6..2611883aeb 100644 --- a/frontend/src/locale/src/lang-list.json +++ b/frontend/src/locale/src/lang-list.json @@ -67,5 +67,8 @@ }, "locale-uk-UA": { "defaultMessage": "Українська" + }, + "locale-uz-UZ": { + "defaultMessage": "O'zbekcha" } } diff --git a/frontend/src/locale/src/uz.json b/frontend/src/locale/src/uz.json new file mode 100644 index 0000000000..8c9f528d4d --- /dev/null +++ b/frontend/src/locale/src/uz.json @@ -0,0 +1,785 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Qolgan zaxira kodlari: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Ushbu zaxira kodlarini xavfsiz joyda saqlang. Har bir koddan faqat bir marta foydalanish mumkin." + }, + "2fa.disable": { + "defaultMessage": "Ikki bosqichli autentifikatsiyani o'chirish" + }, + "2fa.disable-confirm": { + "defaultMessage": "2FA ni o'chirish" + }, + "2fa.disable-warning": { + "defaultMessage": "Ikki bosqichli autentifikatsiyani o'chirish hisobingiz xavfsizligini kamaytiradi." + }, + "2fa.disabled": { + "defaultMessage": "O'chirilgan" + }, + "2fa.done": { + "defaultMessage": "Men zaxira kodlarimni saqlab qo'ydim" + }, + "2fa.enable": { + "defaultMessage": "Ikki bosqichli autentifikatsiyani yoqish" + }, + "2fa.enabled": { + "defaultMessage": "Yoqilgan" + }, + "2fa.enter-code": { + "defaultMessage": "Tasdiqlash kodini kiriting" + }, + "2fa.enter-code-disable": { + "defaultMessage": "O'chirish uchun tasdiqlash kodini kiriting" + }, + "2fa.regenerate": { + "defaultMessage": "Qayta yaratish" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Zaxira kodlarini qayta yaratish" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Yangi zaxira kodlarini yaratish uchun tasdiqlash kodini kiriting. Eski kodlar bekor qilinadi." + }, + "2fa.secret-key": { + "defaultMessage": "Maxfiy kalit (Secret Key)" + }, + "2fa.setup-instructions": { + "defaultMessage": "Ushbu QR kodni autentifikator ilovangiz bilan skanerlang yoki maxfiy kalitni qo'lda kiriting." + }, + "2fa.status": { + "defaultMessage": "Holati" + }, + "2fa.title": { + "defaultMessage": "Ikki bosqichli autentifikatsiya (2FA)" + }, + "2fa.verify-enable": { + "defaultMessage": "Tasdiqlash va yoqish" + }, + "access-list": { + "defaultMessage": "Kirish ro'yxati" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Qoida} other {Qoidalar}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Foydalanuvchi} other {Foydalanuvchilar}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Kamida 1 ta qoida mavjud bo'lsa, barchasini rad etish qoidasi oxirida qo'shiladi" + }, + "access-list.help.rules-order": { + "defaultMessage": "E'tibor bering, ruxsat berish (allow) va rad etish (deny) qoidalari belgilangan tartibda qo'llaniladi." + }, + "access-list.pass-auth": { + "defaultMessage": "Autentifikatsiyani serverga (Upstream) uzatish" + }, + "access-list.public": { + "defaultMessage": "Ommaviy ochiq" + }, + "access-list.public.subtitle": { + "defaultMessage": "Asosiy autentifikatsiya talab qilinmaydi" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 yoki 192.168.1.0/24 yoki 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Ixtiyoriy bittasiga mos kelish (Satisfy Any)" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Foydalanuvchi} other {Foydalanuvchilar}}, {rules} {rules, plural, one {Qoida} other {Qoidalar}} - Yaratilgan: {date}" + }, + "access-lists": { + "defaultMessage": "Kirish ro'yxatlari" + }, + "action.add": { + "defaultMessage": "Qo'shish" + }, + "action.add-location": { + "defaultMessage": "Joylashuv (Location) qo'shish" + }, + "action.allow": { + "defaultMessage": "Ruxsat berish" + }, + "action.clear": { + "defaultMessage": "Tozalash" + }, + "action.close": { + "defaultMessage": "Yopish" + }, + "action.delete": { + "defaultMessage": "O'chirish" + }, + "action.deny": { + "defaultMessage": "Rad etish" + }, + "action.disable": { + "defaultMessage": "Faolsizlantirish" + }, + "action.download": { + "defaultMessage": "Yuklab olish" + }, + "action.edit": { + "defaultMessage": "Tahrirlash" + }, + "action.enable": { + "defaultMessage": "Faollashtirish" + }, + "action.permissions": { + "defaultMessage": "Ruxsatlar" + }, + "action.renew": { + "defaultMessage": "Yangilash" + }, + "action.view-details": { + "defaultMessage": "Tafsilotlarni ko'rish" + }, + "auditlogs": { + "defaultMessage": "Audit jurnallari" + }, + "auto": { + "defaultMessage": "Avto" + }, + "cancel": { + "defaultMessage": "Bekor qilish" + }, + "certificate": { + "defaultMessage": "Sertifikat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Sertifikat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Sertifikat kaliti (Key)" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Oraliq sertifikat (Intermediate Certificate)" + }, + "certificate.in-use": { + "defaultMessage": "Ishlatilmoqda" + }, + "certificate.none.subtitle": { + "defaultMessage": "Sertifikat biriktirilmagan" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Ushbu host HTTPS dan foydalanmaydi" + }, + "certificate.none.title": { + "defaultMessage": "Mavjud emas" + }, + "certificate.not-in-use": { + "defaultMessage": "Ishlatilmayapti" + }, + "certificate.renew": { + "defaultMessage": "Sertifikatni yangilash" + }, + "certificates": { + "defaultMessage": "Sertifikatlar" + }, + "certificates.custom": { + "defaultMessage": "Maxsus sertifikat" + }, + "certificates.custom.warning": { + "defaultMessage": "Parol bilan himoyalangan kalit fayllari qo'llab-quvvatlanmaydi." + }, + "certificates.dns.credentials": { + "defaultMessage": "Kirish ma'lumotlari fayli tarkibi" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ushbu plagin provayderingiz uchun API tokeni yoki boshqa ma'lumotlarni o'z ichiga olgan konfiguratsiya faylini talab qiladi" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Ushbu ma'lumotlar ma'lumotlar bazasida va faylda ochiq matn (plaintext) sifatida saqlanadi!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "DNS tarqalish vaqti (soniya)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Plaginning standart qiymatidan foydalanish uchun bo'sh qoldiring. DNS tarqalishini kutish soniyalari." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS provayder" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Provayderni tanlang..." + }, + "certificates.dns.warning": { + "defaultMessage": "Ushbu bo'lim Certbot va uning DNS plaginlari haqida ma'lumot talab qiladi. Tegishli plaginlar hujjatlari bilan tanishing." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Ushbu domenda server topildi, lekin u Nginx Proxy Manager emasdek ko'rinadi. Domen NPM ishlayotgan IP-ga yo'naltirilganligiga ishonch hosil qiling." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "site24x7.com bilan aloqa xatosi tufayli tekshirish muvaffaqiyatsiz tugadi." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Ushbu domenda server mavjud emas. Domen mavjudligini va NPM ishlayotgan IP-ga yo'naltirilganligini hamda 80-port yo'naltirilganligini tekshiring." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Serveringiz ochiq va sertifikat yaratish imkoniyati mavjud." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Ushbu domenda server topildi, lekin kutilmagan holat kodi qaytdi: {code}. Bu NPM serverimi? Domen NPM IP-siga yo'naltirilganligiga ishonch hosil qiling." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Ushbu domenda server topildi, lekin kutilmagan ma'lumot qaytdi. Bu NPM serverimi? Domen NPM IP-siga yo'naltirilganligiga ishonch hosil qiling." + }, + "certificates.http.test-results": { + "defaultMessage": "Sinov natijalari" + }, + "certificates.http.warning": { + "defaultMessage": "Ushbu domenlar ushbu o'rnatmaga yo'naltirilgan holda sozlangan bo'lishi kerak." + }, + "certificates.key-type": { + "defaultMessage": "Kalit turi" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA keng moslikka ega, ECDSA tezroq va xavfsizroq, lekin eski tizimlarda qo'llab-quvvatlanmasligi mumkin" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "Let's Encrypt orqali" + }, + "certificates.request.title": { + "defaultMessage": "Yangi sertifikat so'rash" + }, + "column.access": { + "defaultMessage": "Kirish" + }, + "column.authorization": { + "defaultMessage": "Avtorizatsiya" + }, + "column.authorizations": { + "defaultMessage": "Avtorizatsiyalar" + }, + "column.custom-locations": { + "defaultMessage": "Maxsus joylashuvlar" + }, + "column.destination": { + "defaultMessage": "Manzil" + }, + "column.details": { + "defaultMessage": "Tafsilotlar" + }, + "column.email": { + "defaultMessage": "Elektron pochta" + }, + "column.event": { + "defaultMessage": "Hodisa" + }, + "column.expires": { + "defaultMessage": "Muddati tugaydi" + }, + "column.http-code": { + "defaultMessage": "HTTP kodi" + }, + "column.incoming-port": { + "defaultMessage": "Kiruvchi port" + }, + "column.name": { + "defaultMessage": "Nomi" + }, + "column.protocol": { + "defaultMessage": "Protokol" + }, + "column.provider": { + "defaultMessage": "Provayder" + }, + "column.roles": { + "defaultMessage": "Rollar" + }, + "column.rules": { + "defaultMessage": "Qoidalar" + }, + "column.satisfy": { + "defaultMessage": "Shart" + }, + "column.satisfy-all": { + "defaultMessage": "Barchasi" + }, + "column.satisfy-any": { + "defaultMessage": "Ixtiyoriy" + }, + "column.scheme": { + "defaultMessage": "Sxema" + }, + "column.source": { + "defaultMessage": "Manba" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Holati" + }, + "created-on": { + "defaultMessage": "Yaratilgan: {date}" + }, + "dashboard": { + "defaultMessage": "Boshqaruv paneli" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Hostlar" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hostlar}}" + }, + "disabled": { + "defaultMessage": "O'chirilgan" + }, + "domain-names": { + "defaultMessage": "Domen nomlari" + }, + "domain-names.max": { + "defaultMessage": "Maksimal {count} ta domen nomi" + }, + "domain-names.placeholder": { + "defaultMessage": "Domen qo'shish uchun yozishni boshlang..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Ushbu tur uchun umumiy (wildcard) domenlarga ruxsat berilmaydi" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Ushbu CA uchun umumiy (wildcard) domenlar qo'llab-quvvatlanmaydi" + }, + "domains.advanced": { + "defaultMessage": "Qo'shimcha sozlamalar" + }, + "domains.force-ssl": { + "defaultMessage": "SSL-ni majburlash (Force SSL)" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS yoqilgan" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS quyi domenlar uchun" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 qo'llab-quvvatlash" + }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Upstream Forwarded Proto sarlavhalariga ishonish" + }, + "domains.use-dns": { + "defaultMessage": "DNS Challenge dan foydalanish" + }, + "email-address": { + "defaultMessage": "Elektron pochta manzili" + }, + "empty-search": { + "defaultMessage": "Natijalar topilmadi" + }, + "empty-subtitle": { + "defaultMessage": "Nega bittasini yaratmaysiz?" + }, + "enabled": { + "defaultMessage": "Yoqilgan" + }, + "error.access.at-least-one": { + "defaultMessage": "Kamida bitta Avtorizatsiya yoki Kirish qoidasi talab qilinadi" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Avtorizatsiya foydalanuvchi nomlari takrorlanmasligi kerak" + }, + "error.invalid-auth": { + "defaultMessage": "Elektron pochta yoki parol noto'g'ri" + }, + "error.invalid-domain": { + "defaultMessage": "Noto'g'ri domen: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Noto'g'ri elektron pochta manzili" + }, + "error.max-character-length": { + "defaultMessage": "Maksimal uzunlik: {max} ta belgi" + }, + "error.max-domains": { + "defaultMessage": "Domenlar juda ko'p, maksimal: {max}" + }, + "error.maximum": { + "defaultMessage": "Maksimal qiymat: {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimal uzunlik: {min} ta belgi" + }, + "error.minimum": { + "defaultMessage": "Minimal qiymat: {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Parollar mos kelishi kerak" + }, + "error.required": { + "defaultMessage": "Ushbu maydon to'ldirilishi shart" + }, + "expires.on": { + "defaultMessage": "Muddati: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Github-da fork qilish" + }, + "host.flags.block-exploits": { + "defaultMessage": "Keng tarqalgan hujumlarni bloklash (Block Exploits)" + }, + "host.flags.cache-assets": { + "defaultMessage": "Statik fayllarni keshlash (Cache Assets)" + }, + "host.flags.preserve-path": { + "defaultMessage": "Yo'lni saqlab qolish (Preserve Path)" + }, + "host.flags.protocols": { + "defaultMessage": "Protokollar" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets qo'llab-quvvatlash" + }, + "host.forward-port": { + "defaultMessage": "Yo'naltirish porti (Forward Port)" + }, + "host.forward-scheme": { + "defaultMessage": "Sxema (Scheme)" + }, + "hosts": { + "defaultMessage": "Hostlar" + }, + "http-only": { + "defaultMessage": "Faqat HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt (DNS orqali)" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt (HTTP orqali)" + }, + "loading": { + "defaultMessage": "Yuklanmoqda…" + }, + "location.advanced-config": { + "defaultMessage": "Maxsus Nginx konfiguratsiyasiga ega" + }, + "location.filter": { + "defaultMessage": "Yo'l yoki manzil bo'yicha filtrlash" + }, + "login.2fa-code": { + "defaultMessage": "Tasdiqlash kodi" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Kodni kiriting" + }, + "login.2fa-description": { + "defaultMessage": "Autentifikator ilovangizdagi kodni kiriting" + }, + "login.2fa-title": { + "defaultMessage": "Ikki bosqichli autentifikatsiya" + }, + "login.2fa-verify": { + "defaultMessage": "Tasdiqlash" + }, + "login.title": { + "defaultMessage": "Hisobingizga kiring" + }, + "nginx-config.label": { + "defaultMessage": "Maxsus Nginx konfiguratsiyasi" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Maxsus Nginx konfiguratsiyangizni bu yerga kiriting (o'z javobgarligingizda)!" + }, + "no-permission-error": { + "defaultMessage": "Sizda buni ko'rish uchun ruxsat yo'q." + }, + "notfound.action": { + "defaultMessage": "Bosh sahifaga qaytish" + }, + "notfound.content": { + "defaultMessage": "Kechirasiz, siz qidirayotgan sahifa topilmadi" + }, + "notfound.title": { + "defaultMessage": "Voy… Sahifa topilmadi" + }, + "notification.error": { + "defaultMessage": "Xatolik" + }, + "notification.object-deleted": { + "defaultMessage": "{object} o'chirildi" + }, + "notification.object-disabled": { + "defaultMessage": "{object} faolsizlantirildi" + }, + "notification.object-enabled": { + "defaultMessage": "{object} faollashtirildi" + }, + "notification.object-renewed": { + "defaultMessage": "{object} yangilandi" + }, + "notification.object-saved": { + "defaultMessage": "{object} saqlandi" + }, + "notification.success": { + "defaultMessage": "Muvaffaqiyatli" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object} qo'shish" + }, + "object.delete": { + "defaultMessage": "{object}ni o'chirish" + }, + "object.delete.content": { + "defaultMessage": "Haqiqatan ham ushbu {object}ni o'chirmoqchimisiz?" + }, + "object.edit": { + "defaultMessage": "{object}ni tahrirlash" + }, + "object.empty": { + "defaultMessage": "{objects} mavjud emas" + }, + "object.event.created": { + "defaultMessage": "{object} yaratildi" + }, + "object.event.deleted": { + "defaultMessage": "{object} o'chirildi" + }, + "object.event.disabled": { + "defaultMessage": "{object} faolsizlantirildi" + }, + "object.event.enabled": { + "defaultMessage": "{object} faollashtirildi" + }, + "object.event.renewed": { + "defaultMessage": "{object} yangilandi" + }, + "object.event.updated": { + "defaultMessage": "{object} yangilandi" + }, + "offline": { + "defaultMessage": "Oflayn" + }, + "online": { + "defaultMessage": "Onlayn" + }, + "options": { + "defaultMessage": "Parametrlar" + }, + "password": { + "defaultMessage": "Parol" + }, + "password.generate": { + "defaultMessage": "Tasodifiy parol yaratish" + }, + "password.hide": { + "defaultMessage": "Parolni yashirish" + }, + "password.show": { + "defaultMessage": "Parolni ko'rsatish" + }, + "permissions.hidden": { + "defaultMessage": "Yashirin" + }, + "permissions.manage": { + "defaultMessage": "Boshqarish" + }, + "permissions.view": { + "defaultMessage": "Faqat ko'rish" + }, + "permissions.visibility.all": { + "defaultMessage": "Barcha elementlar" + }, + "permissions.visibility.title": { + "defaultMessage": "Elementlar ko'rinishi" + }, + "permissions.visibility.user": { + "defaultMessage": "Faqat o'zi yaratgan elementlar" + }, + "proxy-host": { + "defaultMessage": "Proksi host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Yo'naltiriladigan hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proksi hostlar" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proksi host} other {Proksi hostlar}}" + }, + "public": { + "defaultMessage": "Ommaviy" + }, + "redirection-host": { + "defaultMessage": "Qayta yo'naltirish hosti" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Yo'naltiriladigan domen" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP kodi" + }, + "redirection-hosts": { + "defaultMessage": "Qayta yo'naltirish hostlari" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Qayta yo'naltirish hosti} other {Qayta yo'naltirish hostlari}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Bir nechta tanlov" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Doimiy ko'chirilgan" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Vaqtincha ko'chirilgan" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Boshqasiga qarang" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Vaqtincha qayta yo'naltirish" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Doimiy qayta yo'naltirish" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standart foydalanuvchi" + }, + "save": { + "defaultMessage": "Saqlash" + }, + "setting": { + "defaultMessage": "Sozlama" + }, + "settings": { + "defaultMessage": "Sozlamalar" + }, + "settings.default-site": { + "defaultMessage": "Birlamchi sayt" + }, + "settings.default-site.404": { + "defaultMessage": "404 sahifasi" + }, + "settings.default-site.444": { + "defaultMessage": "Javobsiz (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Tabriklaymiz sahifasi" + }, + "settings.default-site.description": { + "defaultMessage": "Nginx noma'lum Host bilan chaqirilganda nima ko'rsatilsin" + }, + "settings.default-site.html": { + "defaultMessage": "Maxsus HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Qayta yo'naltirish" + }, + "setup.preamble": { + "defaultMessage": "Administrator hisobingizni yaratishdan boshlang." + }, + "setup.title": { + "defaultMessage": "Xush kelibsiz!" + }, + "sign-in": { + "defaultMessage": "Kirish" + }, + "ssl-certificate": { + "defaultMessage": "SSL sertifikati" + }, + "stream": { + "defaultMessage": "Oqim (Stream)" + }, + "stream.forward-host": { + "defaultMessage": "Yo'naltiriladigan host" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com yoki 10.0.0.1 yoki 2001:db8::1" + }, + "stream.incoming-port": { + "defaultMessage": "Kiruvchi port" + }, + "streams": { + "defaultMessage": "Oqimlar (Streams)" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Oqim} other {Oqimlar}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Sinash" + }, + "update-available": { + "defaultMessage": "Yangi versiya mavjud: {latestVersion}" + }, + "user": { + "defaultMessage": "Foydalanuvchi" + }, + "user.change-password": { + "defaultMessage": "Parolni o'zgartirish" + }, + "user.confirm-password": { + "defaultMessage": "Parolni tasdiqlang" + }, + "user.current-password": { + "defaultMessage": "Joriy parol" + }, + "user.edit-profile": { + "defaultMessage": "Profilni tahrirlash" + }, + "user.full-name": { + "defaultMessage": "To'liq ism" + }, + "user.login-as": { + "defaultMessage": "{name} sifatida kirish" + }, + "user.logout": { + "defaultMessage": "Chiqish" + }, + "user.new-password": { + "defaultMessage": "Yangi parol" + }, + "user.nickname": { + "defaultMessage": "Taxallus (Nickname)" + }, + "user.set-password": { + "defaultMessage": "Parol o'rnatish" + }, + "user.set-permissions": { + "defaultMessage": "{name} uchun ruxsatlarni o'rnatish" + }, + "user.switch-dark": { + "defaultMessage": "Tungi rejimga o'tish" + }, + "user.switch-light": { + "defaultMessage": "Kunduzgi rejimga o'tish" + }, + "user.two-factor": { + "defaultMessage": "Ikki bosqichli autentifikatsiya" + }, + "username": { + "defaultMessage": "Foydalanuvchi nomi" + }, + "users": { + "defaultMessage": "Foydalanuvchilar" + } +} diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx index d363de9ede..2001e546dd 100644 --- a/frontend/src/modals/PermissionsModal.tsx +++ b/frontend/src/modals/PermissionsModal.tsx @@ -209,7 +209,11 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { )} - {!isAdmin && ( + {isAdmin ? ( + + : + + ) : ( <>
-
+
-
+
-
+
From c819d5dc9c415d90ef5a59aeec3b11d208e2f452 Mon Sep 17 00:00:00 2001 From: Abdulhafiz Davlatov Date: Sun, 6 Sep 2026 23:22:11 +0500 Subject: [PATCH 4/5] Harden admin checks and fix locale tooling Use optional chaining for `access.hasRole` in host permission checks to avoid runtime errors when role helpers are missing, while preserving admin scope behavior. Update frontend locale sorting to run via Node for better cross-platform support, and apply related UI/i18n cleanup (label `htmlFor`, locale key ordering, and trailing newline normalization). --- backend/internal/dead-host.js | 4 ++-- backend/internal/proxy-host.js | 4 ++-- backend/internal/redirection-host.js | 4 ++-- frontend/package.json | 2 +- frontend/src/locale/src/de.json | 2 +- frontend/src/locale/src/en.json | 18 +++++++++--------- frontend/src/locale/src/uz.json | 18 +++++++++--------- frontend/src/modals/PermissionsModal.tsx | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js index 37d98d4d79..c8e0e17cde 100644 --- a/backend/internal/dead-host.js +++ b/backend/internal/dead-host.js @@ -54,7 +54,7 @@ const internalDeadHost = { thisData.advanced_config = ""; } - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") { throw new errs.PermissionError("You do not have permission to modify advanced configuration"); } @@ -156,7 +156,7 @@ const internalDeadHost = { thisData = internalHost.cleanSslHstsData(thisData, row); - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if ( !isAdmin && typeof data.advanced_config !== "undefined" && diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index e5412ff801..ddebe654c0 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -57,7 +57,7 @@ const internalProxyHost = { thisData.advanced_config = ""; } - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") { throw new errs.PermissionError("You do not have permission to modify advanced configuration"); } @@ -188,7 +188,7 @@ const internalProxyHost = { thisData = internalHost.cleanSslHstsData(thisData, row); - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if ( !isAdmin && typeof thisData.advanced_config !== "undefined" && diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js index d33b5ed852..5e0f3a2604 100644 --- a/backend/internal/redirection-host.js +++ b/backend/internal/redirection-host.js @@ -57,7 +57,7 @@ const internalRedirectionHost = { thisData.advanced_config = ""; } - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") { throw new errs.PermissionError("You do not have permission to modify advanced configuration"); } @@ -188,7 +188,7 @@ const internalRedirectionHost = { thisData = internalHost.cleanSslHstsData(thisData, row); - const isAdmin = (access.hasRole && access.hasRole("admin")) || access.token.hasScope("admin"); + const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin"); if ( !isAdmin && typeof thisData.advanced_config !== "undefined" && diff --git a/frontend/package.json b/frontend/package.json index e7c7363c3e..04534a8598 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "prettier": "biome format --write ./src", "locale-extract": "formatjs extract 'src/**/*.tsx'", "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang", - "locale-sort": "./src/locale/scripts/locale-sort.sh", + "locale-sort": "node ./src/locale/scripts/locale-sort.cjs", "test": "vitest" }, "dependencies": { diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json index 6cdb64cf39..c223e4bea2 100644 --- a/frontend/src/locale/src/de.json +++ b/frontend/src/locale/src/de.json @@ -773,4 +773,4 @@ "users": { "defaultMessage": "Benutzer" } -} \ No newline at end of file +} diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 19c2400739..dd157080b4 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -584,24 +584,24 @@ "password.show": { "defaultMessage": "Show Password" }, - "permissions.hidden": { - "defaultMessage": "Hidden" - }, - "permissions.manage": { - "defaultMessage": "Manage" - }, - "permissions.view": { - "defaultMessage": "View Only" - }, "permissions.allowed-proxy-hosts": { "defaultMessage": "Allowed Proxy Hosts" }, "permissions.allowed-proxy-hosts-help": { "defaultMessage": "The user will only be able to view and manage selected proxy hosts (in 'Created Items Only' visibility mode)." }, + "permissions.hidden": { + "defaultMessage": "Hidden" + }, + "permissions.manage": { + "defaultMessage": "Manage" + }, "permissions.no-proxy-hosts": { "defaultMessage": "No proxy hosts found" }, + "permissions.view": { + "defaultMessage": "View Only" + }, "permissions.visibility.all": { "defaultMessage": "All Items" }, diff --git a/frontend/src/locale/src/uz.json b/frontend/src/locale/src/uz.json index ee1697ed5f..1a1e2f4e8d 100644 --- a/frontend/src/locale/src/uz.json +++ b/frontend/src/locale/src/uz.json @@ -584,24 +584,24 @@ "password.show": { "defaultMessage": "Parolni ko'rsatish" }, - "permissions.hidden": { - "defaultMessage": "Yashirin" - }, - "permissions.manage": { - "defaultMessage": "Boshqarish" - }, - "permissions.view": { - "defaultMessage": "Faqat ko'rish" - }, "permissions.allowed-proxy-hosts": { "defaultMessage": "Ruxsat berilgan proksi hostlar" }, "permissions.allowed-proxy-hosts-help": { "defaultMessage": "Foydalanuvchi faqat belgilangan proksi hostlarni ko'rishi va boshqarishi mumkin ('Faqat o'zi yaratgan elementlar' rejimida)." }, + "permissions.hidden": { + "defaultMessage": "Yashirin" + }, + "permissions.manage": { + "defaultMessage": "Boshqarish" + }, "permissions.no-proxy-hosts": { "defaultMessage": "Proksi hostlar mavjud emas" }, + "permissions.view": { + "defaultMessage": "Faqat ko'rish" + }, "permissions.visibility.all": { "defaultMessage": "Barcha elementlar" }, diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx index dfb59f4858..a3e2e152de 100644 --- a/frontend/src/modals/PermissionsModal.tsx +++ b/frontend/src/modals/PermissionsModal.tsx @@ -232,7 +232,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
{values.visibility === "user" && values.proxyHosts !== "hidden" && (
-