diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..d30a72d48 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +.twosky.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 70384e33c..607162bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,13 @@ NOTE: Add new changes BELOW THIS COMMENT. ### Fixed +- Multiple inaccuracies in the OpenAPI specification: + - Wrong property names: `enable` → `enabled` in the Parental status response, `protection_disabled_until` → `protection_disabled_duration` in `ServerStatus`, `ratelimit_subnet_subnet_len_ipv4` and `ratelimit_subnet_subnet_len_ipv6` in `DNSConfig`. + - `upstream_mode` enum in `DNSConfig` changed from object to string format for compatibility with code generators. + - Missing required properties in `DhcpConfigV4` and `DhcpStatus` schemas. + - Missing `port_dnscrypt` and `dnscrypt_config_file` properties in `TlsConfig`. + - Split `NetInterface` into `NetInterface` and `DHCPNetInterface` schemas; `GET /dhcp/interfaces` now uses `DHCPNetInterfaces`. + - Blocked requests without an EDNS(0) OPT record ([#8183]). [#8183]: https://github.com/AdguardTeam/AdGuardHome/issues/8183 diff --git a/client_v2/.eslintrc.cjs b/client_v2/.eslintrc.cjs index 9b21d1c73..963b9e862 100644 --- a/client_v2/.eslintrc.cjs +++ b/client_v2/.eslintrc.cjs @@ -31,6 +31,7 @@ module.exports = { '@typescript-eslint/no-unused-vars': [ 'error', { + varsIgnorePattern: '^_', argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', }, @@ -67,4 +68,13 @@ module.exports = { }, ], }, + overrides: [ + { + // Orval-generated files — max-len is controlled by prettier + files: ['src/api/generated.ts', 'src/api/model/**/*.ts'], + rules: { + 'max-len': 'off', + }, + }, + ], }; diff --git a/client_v2/.prettierignore b/client_v2/.prettierignore index 6d0159ea1..b00dfdba1 100644 --- a/client_v2/.prettierignore +++ b/client_v2/.prettierignore @@ -1 +1,2 @@ ../.twosky.json +src/common/intl/locales.generated.ts diff --git a/client_v2/AGENTS.md b/client_v2/AGENTS.md index a1c15d5ca..4afcbeacb 100644 --- a/client_v2/AGENTS.md +++ b/client_v2/AGENTS.md @@ -2,24 +2,24 @@ ## Table of Contents -- [AGENTS.md — AdGuard Home client\_v2 (SolidJS Frontend)](#agentsmd--adguard-home-client_v2-solidjs-frontend) - - [Table of Contents](#table-of-contents) +- [AGENTS.md — AdGuard Home client_v2 (SolidJS Frontend)](#agentsmd--adguard-home-client_v2-solidjs-frontend) + - [Table of Contents](#table-of-contents) - [Project Overview](#project-overview) - [Technical Context](#technical-context) - [Project Structure](#project-structure) - [Build And Test Commands](#build-and-test-commands) - [Contribution Instructions](#contribution-instructions) - [Code Guidelines](#code-guidelines) - - [System Design](#system-design) - - [Architecture](#architecture) - - [Code Quality](#code-quality) - - [Testing](#testing) - - [Dependency Management](#dependency-management) - - [Configuration \& Documentation](#configuration--documentation) - - [Markdown Formatting](#markdown-formatting) - - [Other](#other) - - [Accessibility](#accessibility) - - [Translations](#translations) + - [System Design](#system-design) + - [Architecture](#architecture) + - [Code Quality](#code-quality) + - [Testing](#testing) + - [Dependency Management](#dependency-management) + - [Configuration \& Documentation](#configuration--documentation) + - [Markdown Formatting](#markdown-formatting) + - [Other](#other) + - [Accessibility](#accessibility) + - [Translations](#translations) # Project Overview @@ -54,7 +54,8 @@ Playwright (e2e). - **State**: SolidJS `createStore` module-scoped stores (no Redux, no Context providers) - **HTTP**: Native `fetch` wrapped in a single `Api` class -- **i18n**: `@adguard/translate` with 13 locale JSON files in `src/__locales/` +- **i18n**: `@adguard/translate` with 35 bundled locales from `src/__locales/` + (generated from `.twosky.json` via `npm run locales:generate`) - **Testing**: Vitest 4 + `@solidjs/testing-library` (unit); Playwright 1.56 (e2e against a real backend) - **Linting/Formatting**: ESLint 8 (`@typescript-eslint`, `eslint-plugin-solid`, @@ -77,7 +78,7 @@ client_v2/ │ ├── index.tsx # Main app entry (renders ) │ ├── index.pcss # Global CSS (vars, reset) │ ├── initialState.ts # Domain model types + initial form state -│ ├── api/ # Single Api class — all HTTP calls +│ ├── api/ # Orval-generated typed client + customFetch mutator │ ├── common/ │ │ ├── controls/ # Input primitives (Checkbox, Input, Select, Switch, …) │ │ ├── ui/ # Higher-level UI (Button, Dialog, Table, Tabs, Sidebar, …) @@ -127,6 +128,8 @@ All commands are run from the `client_v2/` directory. | Unit tests (watch) | `npm run test:watch` | | E2e tests | `npm run test:e2e` | | E2e interactive UI | `npm run test:e2e:interactive` | +| Locale codegen | `npm run locales:generate` | +| Locale freshness check | `npm run locales:check` | | Translation check | `npm run translations:check` | | Full check (lint + typecheck + test) | `npm run check` | @@ -166,9 +169,10 @@ no server process of its own. Design for the browser environment: - **Stateless client, stateful backend.** The UI holds no durable state; all configuration and data persist in the AdGuard Home backend via the `/control` HTTP API. Treat the browser as a thin view layer. -- **Single source of HTTP.** All backend calls go through the singleton - `apiClient` in `src/api/Api.ts`. Do not call `fetch` directly from components - or stores — add a typed method to the `Api` class instead. +- **Single source of HTTP.** All backend calls go through the orval-generated + typed client in `src/api/generated.ts` (with the `customFetch` mutator in + `src/api/customFetch.ts`). Do not call `fetch` directly from components + or stores — import a generated function instead. - **No shared server memory.** Multiple browser tabs / reloads are independent; never rely on in-memory module state surviving a reload. Re-fetch data in component `onMount` rather than assuming a store is already populated. @@ -199,8 +203,9 @@ Universal design principles the codebase follows: → api → backend. Stores never import components; api never imports stores. - **Explicit Boundaries** — modules interact through named exports; no reaching into another module's internals. -- **Data Flow Clarity** — user action → store action function → `apiClient` - → `setState` → reactive UI update. Data moves in one predictable path. +- **Data Flow Clarity** — user action → store action function → generated + API function → `setState` → reactive UI update. Data moves in one + predictable path. - **Minimize Coupling, Maximize Cohesion** — stores are self-contained and imported directly (no Context providers); components depend on narrow store exports. @@ -221,7 +226,7 @@ This project's layers, from top to bottom: | Components | Render UI, handle user interaction | `src/components/Dashboard/`, `src/components/Clients/` | | Common UI | Reusable controls and primitives | `src/common/controls/Select/`, `src/common/ui/Button/` | | Stores | Domain state + async actions | `src/stores/clients.ts`, `src/stores/queryLogs.ts` | -| API | HTTP transport to backend | `src/api/Api.ts` | +| API | HTTP transport to backend | `src/api/generated.ts`, `src/api/customFetch.ts` | | Helpers | Pure utilities, validators | `src/helpers/`, `src/lib/` | ```text @@ -229,7 +234,7 @@ Components (pages, controls) ↓ Stores (domain state + actions) ↓ -API (apiClient → fetch /control) +API (generated functions → customFetch → fetch /control) ↓ AdGuard Home backend (Go) ``` @@ -246,7 +251,7 @@ must not depend on stores or components. Helpers are pure and dependency-free. ## Code Quality - **Path aliases**: Use the `panel/*` alias for all `src/` imports - (e.g. `import { apiClient } from 'panel/api/Api'`). Use `Twosky` for the + (e.g. `import { status } from 'panel/api/generated'`). Use `Twosky` for the root `.twosky.json` config. Avoid deep relative paths (`../../..`). - **Component conventions**: PascalCase directories and component files. Each component lives in its own directory with an `index.tsx` and a @@ -284,12 +289,12 @@ must not depend on stores or components. Helpers are pure and dependency-free. `createMemo` (e.g., table columns), **access the prop in the memo body** so SolidJS tracks the dependency. - **Stores**: Module-scoped `createStore` singletons exported directly — no - Context/Provider. Async actions set a `processing*` flag, call `apiClient`, - then `setState`. Errors are reported via `addErrorToast`. + Context/Provider. Async actions set a `processing*` flag, call generated + API functions, then `setState`. Errors are reported via `addErrorToast`. - **Naming**: Files `PascalCase.tsx` for components, `camelCase.ts` for stores/helpers. CSS module files `*.module.pcss`. -- **Error handling**: API errors throw from `Api.ts`; store actions catch and - surface a toast. Do not swallow errors silently. +- **Error handling**: API errors throw from `customFetch.ts`; store actions + catch and surface a toast. Do not swallow errors silently. - **Logging**: `console.warn` and `console.error` are allowed; `console.log` is disallowed by ESLint. - **Static analysis gates**: ESLint, Prettier, and `tsc --noEmit` must pass. @@ -312,8 +317,8 @@ must not depend on stores or components. Helpers are pure and dependency-free. `@testing-library/jest-dom` matchers. - **Store/helper tests**: Call exported action/builder functions directly and assert on returned values or state (e.g. - `src/__tests__/clientForm/buildClientConfig.test.ts`). Mock `apiClient` when - a test would otherwise hit the network. + `src/__tests__/clientForm/buildClientConfig.test.ts`). Mock generated API + functions when a test would otherwise hit the network. - **Naming**: Mirror the source path under `__tests__/` (e.g. `stores/clients.ts` → `__tests__/stores/clients.test.ts`). - **E2E tests**: Playwright, configured in `playwright.config.ts` @@ -439,6 +444,12 @@ friendly: - All user-facing strings must be localized. Add new keys to the base locale `src/__locales/en.json`; other locales are managed externally via Twosky. +- Locale imports are **generated** — do not hand-edit. + When `.twosky.json` or the set of locale JSON files changes, run: + ```sh + npm run locales:generate + ``` + CI verifies freshness with `npm run locales:check`. - Access translations via the `intl` object from `panel/common/intl`: - `intl.getMessage('key', values?)` — returns a plain localized string. Pass interpolation values as the second argument, referenced inside the diff --git a/client_v2/DEVELOPMENT.md b/client_v2/DEVELOPMENT.md index 91106c8ee..4a40ba6b1 100644 --- a/client_v2/DEVELOPMENT.md +++ b/client_v2/DEVELOPMENT.md @@ -214,6 +214,16 @@ This audits source files for `intl.getMessage` / `intl.getPlural` usage and reports missing, unused, and dynamic keys. Other locales are managed externally via Twosky (configured in the root `.twosky.json`). +When `.twosky.json` changes (a language is added or removed), regenerate the +locale import file: + +```sh +npm run locales:generate +``` + +This writes `src/common/intl/locales.generated.ts`, which is committed. +CI verifies freshness with `npm run locales:check`. + ## Common Tasks ### Adding a Component diff --git a/client_v2/orval.config.ts b/client_v2/orval.config.ts new file mode 100644 index 000000000..ef633f169 --- /dev/null +++ b/client_v2/orval.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'orval'; + +export default defineConfig({ + adguardHome: { + input: { + target: '../openapi/openapi.yaml', + }, + output: { + target: './src/api/generated.ts', + schemas: './src/api/model', + client: 'fetch', + baseUrl: 'control', + prettier: true, + override: { + header: false, + mutator: { + path: './src/api/customFetch.ts', + name: 'customFetch', + }, + fetch: { + includeHttpResponseReturnType: false, + }, + enumGenerationType: 'union', + }, + }, + }, +}); diff --git a/client_v2/package-lock.json b/client_v2/package-lock.json index 9d1eee694..1c9fb3973 100644 --- a/client_v2/package-lock.json +++ b/client_v2/package-lock.json @@ -19,7 +19,6 @@ "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", "js-yaml": "^4.1.0", - "lodash": "^4.17.19", "nanoid": "^5.1.0", "qs": "^6.14.0", "solid-js": "^1.9.0", @@ -33,7 +32,6 @@ "@solidjs/testing-library": "^0.8.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", - "@types/lodash": "^4.17.4", "@types/node": "^22.13.10", "@types/qs": "^6.15.1", "@typescript-eslint/eslint-plugin": "^8.60.1", @@ -54,6 +52,7 @@ "html-webpack-plugin": "^5.6.0", "jsdom": "^26.1.0", "mini-css-extract-plugin": "^2.9.0", + "orval": "^8.22.0", "playwright": "^1.61.0", "postcss": "^8.5.6", "postcss-import": "^16.1.1", @@ -2759,6 +2758,20 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.23.0", + "@shikijs/langs": "^3.23.0", + "@shikijs/themes": "^3.23.0", + "@shikijs/types": "^3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3449,6 +3462,648 @@ "node": ">=12.4.0" } }, + "node_modules/@orval/angular": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-8.22.0.tgz", + "integrity": "sha512-lSvaNj+VHGIWBQOG104HfPNrk2xGjpArwy67u6ZBPzHo/OtDE5Tm1t+ARYseCOElFr4z3i3A62qjFLFy6zj00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0" + } + }, + "node_modules/@orval/axios": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-8.22.0.tgz", + "integrity": "sha512-fFu0UgTbpI9a1ayM7qCs55MMy6MlVgOpE3BBXGukCTBSwNwFLN47WdzQev/x0mAcs58pNKYT0KL4R9MXGnmgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0" + } + }, + "node_modules/@orval/core": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-8.22.0.tgz", + "integrity": "sha512-uKYi7+Smg6oQ2MxE0AS2FNI7bwssoxGLh391Uk0FU+DcTcSv9q2GmvoM8uwd827Hok29kD7AegHE8Mhmsa5uWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.8.0", + "acorn": "^8.15.0", + "compare-versions": "^6.1.1", + "debug": "^4.4.3", + "esbuild": "^0.28.0", + "esutils": "2.0.3", + "fs-extra": "^11.3.2", + "jiti": "^2.6.1", + "jsesc": "^3.0.0", + "remeda": "^2.33.6", + "tinyglobby": "^0.2.16", + "typedoc": "^0.28.19" + }, + "peerDependencies": { + "@faker-js/faker": ">=10" + }, + "peerDependenciesMeta": { + "@faker-js/faker": { + "optional": true + } + } + }, + "node_modules/@orval/core/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@orval/core/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/@orval/effect": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/effect/-/effect-8.22.0.tgz", + "integrity": "sha512-DACiw2+0ZsPJPKQbYlb9qFFFppaCBaT/HgIq2S1asH9ZCOOTZKm9VrRXpeCWINHC2w1BpdSATpytv+61UT5Y/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "remeda": "^2.33.6" + } + }, + "node_modules/@orval/fetch": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-8.22.0.tgz", + "integrity": "sha512-G0r6hOdZG963H/8S4Vk1kWfU/hkQC/cwpDZL+mzcXgzrdG9NDloshTqmge/jP5lPsFAwfcRUmmts10OmuIE4BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0" + } + }, + "node_modules/@orval/hono": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-8.22.0.tgz", + "integrity": "sha512-GMCpGZqCuGYSu10KTt2q3WYzCqEcTGxr18z3HgHv9dX22sv/V76dlLBh3SE72xp8Z7/jmoq3GKBdgU9k98ju0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "@orval/zod": "8.22.0", + "fs-extra": "^11.3.2" + }, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@orval/mcp": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-8.22.0.tgz", + "integrity": "sha512-ySa4EenAF8YMCpbmddJGO3lItTPC8Uf/iT+P2ezaowVgxOCPX/fjG7dd0fnVmrr0vaphi53yWx5o5DlT17FzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "@orval/fetch": "8.22.0", + "@orval/zod": "8.22.0" + } + }, + "node_modules/@orval/mock": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-8.22.0.tgz", + "integrity": "sha512-ZCEFpdi4z+YOCg0Tsgz6DO/M1TSfYxE5urDWJgYNUKeD8XDEHFb2IgTiiaAwJunqWnRQuiMfGp9G81+u10Kx6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "remeda": "^2.33.6" + } + }, + "node_modules/@orval/query": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-8.22.0.tgz", + "integrity": "sha512-IH9049z860CLUeGEezGv+yOvUvcAyDnd9doDe1Ryi0WxsDul2Vh3LjCRUEQf4JZiyZezadWYYGzs0lwnCn/afA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "@orval/fetch": "8.22.0", + "remeda": "^2.33.6" + } + }, + "node_modules/@orval/solid-start": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/solid-start/-/solid-start-8.22.0.tgz", + "integrity": "sha512-Q1ZCWOrA6/VJnyj62XpPxXti9NpAA4lDZVqPL2uQ5gbxv+T+azEaazKqYIpSBJXbl1aEezujdcyg/DVUakWKEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0" + } + }, + "node_modules/@orval/swr": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-8.22.0.tgz", + "integrity": "sha512-OSeWX3Af8ESapKIo3XpbOaTMaG8oeEtuucFyOUauqg6NyC6/9Ikcf2csBdF0AuIKA0QTcjcXxra/ccj1f7KnRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "@orval/fetch": "8.22.0" + } + }, + "node_modules/@orval/zod": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-8.22.0.tgz", + "integrity": "sha512-briHtUTz79fvCeIFfGW3IbmUIF9DOotUoWFa/sKUjRUG8PMGDWSjQzO8QdLzLoETVGo9g4TfGwp5Xjcs81GZTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.22.0", + "jsesc": "^3.0.0", + "remeda": "^2.33.6" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", @@ -4036,6 +4691,222 @@ "dev": true, "license": "MIT" }, + "node_modules/@scalar/helpers": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.9.2.tgz", + "integrity": "sha512-hjyMpMZjTBZQhyByZmz5oUgRKUQJO5V5AOiJxsVEGbUmgA7sJRQeTrXLB+BEwzaKS5nm2opJeNyMBYLFNK4hiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/json-magic": { + "version": "0.12.19", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.12.19.tgz", + "integrity": "sha512-1T4QoFYZ1nKt25xFeHtghAuZzaLq2X4CpCSLFXG0Fjcz6K2HIZqo+RtywfI0WD8RRRRgS34keo7X4Gv1BQUNoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.9.2", + "pathe": "^2.0.3", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser": { + "version": "0.28.10", + "resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.28.10.tgz", + "integrity": "sha512-jn3ftvtNTcWOgxf7XVn9CvJGMjFS7QU1b6FiGmibzAlR/6pOP3Ei7sBBnfIY4PBwHjHgMAGBoGApfOV8h75gPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.9.2", + "@scalar/json-magic": "0.12.19", + "@scalar/openapi-types": "0.9.3", + "@scalar/openapi-upgrader": "0.2.11", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "jsonpointer": "^5.0.1", + "leven": "^4.0.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/@scalar/openapi-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.3.tgz", + "integrity": "sha512-34qglt5jSo55iZfH9i7EhjQCdE0Po2xZeh8wytQKolSnXrxsYMSyFDJEBxz1Gaew4on9N3XIWsd0QpwKVA5CSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@scalar/openapi-parser/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@scalar/openapi-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scalar/openapi-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.8.0.tgz", + "integrity": "sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.2.11.tgz", + "integrity": "sha512-eYEFBO8mZfgXEO/hv8rdL5OA4oOB8orFT5kXNK4I/x9xca2D7A4BteuFXqRaw9lE1CIjtG+StlAUYVz5omTXew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.9.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader/node_modules/@scalar/openapi-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.3.tgz", + "integrity": "sha512-34qglt5jSo55iZfH9i7EhjQCdE0Po2xZeh8wytQKolSnXrxsYMSyFDJEBxz1Gaew4on9N3XIWsd0QpwKVA5CSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@solid-primitives/keyed": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.3.tgz", @@ -4350,6 +5221,16 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4388,13 +5269,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4493,6 +5367,13 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -7434,6 +8315,13 @@ "dev": true, "license": "ISC" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -9056,6 +9944,46 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -9238,6 +10166,22 @@ "node": ">=0.8.0" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9437,6 +10381,21 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9559,6 +10518,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -10221,6 +11197,16 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -10948,6 +11934,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -10999,6 +11998,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -11262,6 +12274,29 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kebab-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", @@ -11307,6 +12342,19 @@ "shell-quote": "^1.8.4" } }, + "node_modules/leven": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-4.1.0.tgz", + "integrity": "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11328,6 +12376,26 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -11362,6 +12430,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -11402,6 +12471,13 @@ "dev": true, "license": "ISC" }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -11422,6 +12498,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11449,6 +12566,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11818,6 +12942,36 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -12051,6 +13205,186 @@ "node": ">= 0.8.0" } }, + "node_modules/orval": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/orval/-/orval-8.22.0.tgz", + "integrity": "sha512-N8UmB4DOhW+Z2SzVq4oS/pN3DsRPq+msrRU8NyRldP1i0yiqT6qo8mUxHPk2umqYjyY0FlR2mvPbi/Jf5CiP7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@orval/angular": "8.22.0", + "@orval/axios": "8.22.0", + "@orval/core": "8.22.0", + "@orval/effect": "8.22.0", + "@orval/fetch": "8.22.0", + "@orval/hono": "8.22.0", + "@orval/mcp": "8.22.0", + "@orval/mock": "8.22.0", + "@orval/query": "8.22.0", + "@orval/solid-start": "8.22.0", + "@orval/swr": "8.22.0", + "@orval/zod": "8.22.0", + "@scalar/json-magic": "^0.12.16", + "@scalar/openapi-parser": "^0.28.7", + "@scalar/openapi-types": "0.8.0", + "chokidar": "^5.0.0", + "commander": "^15.0.0", + "execa": "^9.6.1", + "find-up": "8.0.0", + "fs-extra": "^11.3.2", + "get-tsconfig": "^4.14.0", + "jiti": "^2.6.1", + "js-yaml": "4.2.0", + "remeda": "^2.33.6", + "string-argv": "^0.3.2", + "typedoc": "^0.28.19", + "typedoc-plugin-coverage": "^4.0.2", + "typedoc-plugin-markdown": "^4.10.0" + }, + "bin": { + "orval": "dist/bin/orval.mjs" + }, + "engines": { + "node": ">=22.18.0" + }, + "peerDependencies": { + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/orval/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/orval/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/orval/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/orval/node_modules/find-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz", + "integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^8.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval/node_modules/locate-path": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz", + "integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/orval/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12182,6 +13516,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -12849,6 +14196,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12905,6 +14268,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -13233,6 +14606,19 @@ "node": ">= 0.10" } }, + "node_modules/remeda": { + "version": "2.39.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.39.0.tgz", + "integrity": "sha512-3Ki8dU1o3OVu4dwIQ2Pj+yiuP7OnEbmWAGmJ3yDRqopily5jsj8NWzPvbS89H85d6UdONKEcUnrfuHY6jN9vyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -14209,6 +15595,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -14307,6 +15703,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -15035,6 +16444,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedoc": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, + "node_modules/typedoc-plugin-coverage": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-coverage/-/typedoc-plugin-coverage-4.0.3.tgz", + "integrity": "sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.12.0.tgz", + "integrity": "sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -15049,6 +16508,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -15119,6 +16585,29 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -16130,6 +17619,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -16142,6 +17647,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/client_v2/package.json b/client_v2/package.json index e485b22df..259f30663 100644 --- a/client_v2/package.json +++ b/client_v2/package.json @@ -19,7 +19,10 @@ "test:e2e:codegen": "npx playwright codegen", "typecheck": "tsc --noEmit", "typecheck:watch": "tsc --noEmit --watch", - "translations:check": "node ./scripts/check-translations.js" + "translations:check": "node ./scripts/check-translations.js", + "locales:generate": "node ./scripts/generate-locales.js", + "locales:check": "node ./scripts/check-locales.js", + "api:generate": "orval --config ./orval.config.ts && bash scripts/postgenerate.sh" }, "type": "module", "dependencies": { @@ -34,7 +37,6 @@ "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", "js-yaml": "^4.1.0", - "lodash": "^4.17.19", "nanoid": "^5.1.0", "qs": "^6.14.0", "solid-js": "^1.9.0", @@ -48,7 +50,6 @@ "@solidjs/testing-library": "^0.8.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", - "@types/lodash": "^4.17.4", "@types/node": "^22.13.10", "@types/qs": "^6.15.1", "@typescript-eslint/eslint-plugin": "^8.60.1", @@ -69,6 +70,7 @@ "html-webpack-plugin": "^5.6.0", "jsdom": "^26.1.0", "mini-css-extract-plugin": "^2.9.0", + "orval": "^8.22.0", "playwright": "^1.61.0", "postcss": "^8.5.6", "postcss-import": "^16.1.1", diff --git a/client_v2/scripts/check-locales.js b/client_v2/scripts/check-locales.js new file mode 100644 index 000000000..2a1ae6e78 --- /dev/null +++ b/client_v2/scripts/check-locales.js @@ -0,0 +1,54 @@ +/** + * CI freshness guard for `src/common/intl/locales.generated.ts`. + * + * Generates the file to a temp location and compares it against the + * committed version. No working-tree side effects — the real file + * is never modified by this script. + * + * Usage: + * node ./scripts/check-locales.js + * npm run locales:check + */ + +import { execFileSync } from 'node:child_process'; +import { readFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const GEN_SCRIPT = resolve(__dirname, 'generate-locales.js'); +const REAL_FILE = resolve(__dirname, '..', 'src', 'common', 'intl', 'locales.generated.ts'); +const TMP_FILE = resolve(tmpdir(), 'locales.generated.ts'); + +let exitCode = 0; + +try { + // Generate to a temp file so we never touch the real working tree. + execFileSync(process.execPath, [GEN_SCRIPT, '--out', TMP_FILE], { stdio: 'inherit' }); + + // Compare contents directly — the file is small enough that a full + // string comparison is both fast and precise. + const generated = readFileSync(TMP_FILE, 'utf-8'); + const committed = readFileSync(REAL_FILE, 'utf-8'); + + if (generated !== committed) { + console.error(''); + console.error('ERROR: locales.generated.ts is out of date.'); + console.error(' Run: npm run locales:generate'); + console.error(''); + exitCode = 1; + } else { + console.log('locales.generated.ts is up to date.'); + } +} finally { + // Always clean up the temp file, even when the check fails. + try { + unlinkSync(TMP_FILE); + } catch { + // Best effort — the file might not exist if generate-locales itself + // failed early. + } +} + +process.exit(exitCode); diff --git a/client_v2/scripts/check-translations.js b/client_v2/scripts/check-translations.js index f5e6c9371..b39937913 100644 --- a/client_v2/scripts/check-translations.js +++ b/client_v2/scripts/check-translations.js @@ -8,8 +8,11 @@ import { auditTranslations, collectTranslationUsageFromFiles, formatAuditReport, + formatPluralReport, listSourceFiles, loadLocaleMessages, + loadTwoskyLocales, + validatePlurals, } from './translation-audit.js'; export const runTranslationAudit = async ({ @@ -29,7 +32,13 @@ export const runTranslationAudit = async ({ const usage = await collectTranslationUsageFromFiles(filePaths); const report = auditTranslations({ localeMessages, usage }); - write(`${formatAuditReport(report, { rootDir })}\n`); + write(`${formatAuditReport(report, { rootDir })}\n\n`); + + const repoRoot = path.resolve(rootDir, '..'); + const localesDir = path.join(srcDir, '__locales'); + const supportedLocales = await loadTwoskyLocales(repoRoot); + const pluralErrors = await validatePlurals(localesDir, supportedLocales); + write(`${formatPluralReport(pluralErrors)}\n`); return 0; } catch (error) { diff --git a/client_v2/scripts/generate-locales.js b/client_v2/scripts/generate-locales.js new file mode 100644 index 000000000..c70e2c10e --- /dev/null +++ b/client_v2/scripts/generate-locales.js @@ -0,0 +1,129 @@ +/** + * Generates `src/common/intl/locales.generated.ts` from the Twosky language + * list in `.twosky.json` (project: `home_v2`). + * + * Usage: + * node ./scripts/generate-locales.js # regenerate the file + * npm run locales:generate # same, via package.json + * + * CI freshness guard: + * npm run locales:check # fails if output is stale + * + * The generated file contains static import statements and a LOCALES map so + * that webpack can tree-shake each locale JSON bundle. Every locale from + * `.twosky.json` → `home_v2.languages` is included except those listed in + * EXCLUDES below. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// --- Configuration --- + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(__dirname, '..', '..'); +const TWOSKY_PATH = resolve(REPO_ROOT, '.twosky.json'); + +// Allow the caller to override the output path via `--out `. +// Useful for CI checks that want to generate to a temp file. +const args = process.argv.slice(2); +const outFlagIdx = args.indexOf('--out'); +const OUTPUT_PATH = + outFlagIdx !== -1 && args[outFlagIdx + 1] + ? resolve(args[outFlagIdx + 1]) + : resolve(__dirname, '..', 'src', 'common', 'intl', 'locales.generated.ts'); + +// --- Helpers --- + +/** Turn a hyphenated locale code into a valid JavaScript identifier. + * E.g. "pt-br" → "ptBr", "zh-hk" → "zhHk", "sr-cs" → "srCs". + */ +const toAlias = (code) => code.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + +// --- Main --- + +try { + const twoskyRaw = readFileSync(TWOSKY_PATH, 'utf-8'); + const twosky = JSON.parse(twoskyRaw); + + const homeV2 = twosky.find((p) => p.project_id === 'home_v2'); + if (!homeV2) { + console.error('ERROR: no project with project_id "home_v2" found in .twosky.json'); + process.exit(1); + } + + const allCodes = Object.keys(homeV2.languages); + // Stable sort for deterministic output + allCodes.sort(); + + // Verify every locale JSON file exists before writing output + const localesDir = resolve(__dirname, '..', 'src', '__locales'); + for (const code of allCodes) { + const jsonPath = resolve(localesDir, `${code}.json`); + try { + readFileSync(jsonPath); + } catch { + console.error( + `ERROR: locale JSON file not found: ${jsonPath}` + + `\n Ensure the file was downloaded from Twosky before regenerating.`, + ); + process.exit(1); + } + } + + // --- Emit --- + + const baseLocale = homeV2.base_locale || 'en'; + const lazyCodes = allCodes.filter((code) => code !== baseLocale); + + const lines = [ + '/* eslint-disable */', + '// AUTO-GENERATED by `npm run locales:generate`. Do not edit by hand.', + '// Source: .twosky.json → project_id: home_v2 → languages', + '// Excludes: (none)', + '', + 'export type LocaleMessage = Record;', + '', + ]; + + // Static import for the base locale (always bundled, synchronous fallback) + const baseAlias = toAlias(baseLocale); + lines.push(`import ${baseAlias} from 'panel/__locales/${baseLocale}.json';`); + lines.push(''); + + // Lazy-loaders for every non-base locale. + // Return type is Promise because JSON dynamic imports produce + // { default: LocaleMessage }; the preloadLocale() helper unwraps + // the .default at runtime. + lines.push('export const LOCALE_LOADERS: Record Promise> = {'); + for (let i = 0; i < lazyCodes.length; i++) { + const code = lazyCodes[i]; + const comma = i === lazyCodes.length - 1 ? '' : ','; + lines.push( + ` '${code}': () => import(/* webpackChunkName: "locale.${code}" */ 'panel/__locales/${code}.json')${comma}`, + ); + } + lines.push('};'); + lines.push(''); + + // All known locale codes (used by resolveLanguage to validate codes + // before messages are loaded) + const codeList = allCodes.map((c) => `'${c}'`).join(', '); + lines.push(`export const LOCALE_CODES = new Set([${codeList}]);`); + lines.push(''); + + // Synchronously available locale data — starts with only the base + // locale, expanded by preloadLocale() at runtime. + lines.push('export const LOCALES: Record = {'); + lines.push(` ${baseLocale}: ${baseAlias},`); + lines.push('};'); + lines.push(''); + + writeFileSync(OUTPUT_PATH, lines.join('\n') + '\n', 'utf-8'); + + console.log(`Wrote ${OUTPUT_PATH} (${allCodes.length} locales)`); +} catch (err) { + console.error('Failed to generate locales.generated.ts:', err.message); + process.exit(1); +} diff --git a/client_v2/scripts/postgenerate.sh b/client_v2/scripts/postgenerate.sh new file mode 100755 index 000000000..77f0059d4 --- /dev/null +++ b/client_v2/scripts/postgenerate.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +npx prettier --write "src/api/generated.ts" "src/api/model/**/*.ts" +npx eslint --quiet --fix "src/api/generated.ts" "src/api/model/**/*.ts" || true diff --git a/client_v2/scripts/translation-audit.js b/client_v2/scripts/translation-audit.js index 3c98f56f9..b678ac063 100644 --- a/client_v2/scripts/translation-audit.js +++ b/client_v2/scripts/translation-audit.js @@ -1,11 +1,63 @@ import fs from 'node:fs/promises'; import path from 'node:path'; +import { validator } from '@adguard/translate'; import ts from 'typescript'; const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx']); const TRANSLATION_METHODS = new Set(['getMessage', 'getPlural']); +/** + * Loads the locale-to-underscore map from .twosky.json in the repo root. + * Returns a Map where keys are hyphenated locale codes (e.g. "pt-br") and + * values are @adguard/translate locale codes (e.g. "pt_br"), or `null` for + * unsupported locales. + * + * @param {string} repoRoot - Path to the repository root (contains .twosky.json). + * @returns {Promise>} + */ +export const loadTwoskyLocales = async (repoRoot) => { + const twoskyPath = path.join(repoRoot, '.twosky.json'); + const raw = await fs.readFile(twoskyPath, 'utf8'); + const projects = JSON.parse(raw); + + const homeV2 = projects.find((p) => p.project_id === 'home_v2'); + if (!homeV2) { + throw new Error('home_v2 project not found in .twosky.json'); + } + + /** + * Converts a hyphenated twosky locale code to @adguard/translate format. + * Most become underscore (pt-br → pt_br). Special cases for locales that + * the library handles under a parent code, or doesn't handle at all. + */ + const toTranslateLocale = (code) => { + // Sinhala → Amharic: identical CLDR plural rules (one: n=0..1, other) + // This only affects plural-form indexing; actual strings come from si-lk.json + // + // TODO(ik): Contribute missing `si` locale to @adguard/translate + if (code === 'si-lk') { + return 'am'; + } + // Hong Kong & Serbian Cyrillic → parent locale + if (code === 'zh-hk') { + return 'zh'; + } + if (code === 'sr-cs') { + return 'sr'; + } + + return code.replace(/-/g, '_'); + }; + + const map = new Map(); + for (const code of Object.keys(homeV2.languages)) { + map.set(code, toTranslateLocale(code)); + } + + return map; +}; + const isIntlMethod = (node) => { if (!ts.isPropertyAccessExpression(node)) { return false; @@ -216,3 +268,80 @@ export const collectTranslationUsageFromFiles = async (filePaths) => { return usage; }; + +/** + * Validates plural forms in all locale files against the @adguard/translate + * library's plural rules (CLDR-based). Returns an array of errors grouped by + * locale file and key. + * + * @param {string} localesDir - Path to __locales directory + * @param {{write?: (chunk: string) => void}} [options] + * @returns {Promise>} + */ +export const validatePlurals = async (localesDir, supportedLocales) => { + const errors = []; + const entries = await fs.readdir(localesDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.json')) { + continue; + } + + const localeName = entry.name.replace(/\.json$/, ''); + const localeCode = supportedLocales.get(localeName); + + // `undefined` means not in twosky list; `null` means unsupported + if (localeCode === undefined || localeCode === null) { + continue; + } + + const localePath = path.join(localesDir, entry.name); + const messages = await loadLocaleMessages(localePath); + + for (const [key, value] of Object.entries(messages)) { + // Only validate strings that START with "|" (plural form separator). + // Strings containing "||" (like filter rule examples) are not plurals. + if (typeof value !== 'string' || !value.trimStart().startsWith('|')) { + continue; + } + + if (!validator.isPluralFormValid(value, localeCode, key)) { + errors.push({ file: entry.name, key, value }); + } + } + } + + return errors; +}; + +/** + * Formats the plural validation report. + * + * @param {Array<{file: string, key: string, value: string}>} errors + * @returns {string} + */ +export const formatPluralReport = (errors) => { + if (errors.length === 0) { + return 'Plural form validation: all locales passed'; + } + + const byFile = new Map(); + for (const err of errors) { + const list = byFile.get(err.file) || []; + list.push(err); + byFile.set(err.file, list); + } + + const lines = [`Plural form errors: ${errors.length} keys`]; + + for (const [file, errs] of byFile) { + lines.push(`\n ${file}:`); + for (const { key, value } of errs) { + const pipeCount = (value.match(/\|/g) || []).length; + lines.push(` ${key} (${pipeCount} pipes)`); + lines.push(` "${value.slice(0, 80)}${value.length > 80 ? '…' : ''}"`); + } + } + + return lines.join('\n'); +}; diff --git a/client_v2/src/__locales/en.json b/client_v2/src/__locales/en.json index e9516f2bd..df96407bd 100644 --- a/client_v2/src/__locales/en.json +++ b/client_v2/src/__locales/en.json @@ -209,6 +209,7 @@ "dns_blocking_mode_ttl_label": "Blocked response TTL, in seconds", "dns_blocking_mode_ttl_placeholder": "Enter TTL", "dns_bootstrap_dns_desc": "Sets the DNS servers used to resolve the hostnames of DoH/DoT upstream resolvers", + "dns_bootstrap_dns_desc_2": "Enter one IP address per line. Comments must start on a new line with #", "dns_bootstrap_dns_label": "Bootstrap DNS servers, one per line", "dns_bootstrap_dns_placeholder": "IP addresses", "dns_bootstrap_dns_title": "Bootstrap DNS servers", @@ -448,7 +449,6 @@ "install_auth_password_enter": "Enter password", "install_auth_username": "Username", "install_auth_username_enter": "Enter username", - "install_saved": "Saved successfully", "install_settings_all_interfaces": "All interfaces", "install_settings_port": "Port", "install_static_configure": "AdGuard Home has detected that the dynamic IP address %ip% is used. Do you want it to be set as your static address?", @@ -503,7 +503,7 @@ "parental_group_software": "Software development", "parental_group_streaming": "Streaming", "password_label": "Password", - "password_login_error": "Your username and password do not match", + "password_login_error": "Unrecognized password", "password_placeholder": "Enter password", "password_requirements": "Password requirements", "password_requirements_characters": "Minimum 8 characters", @@ -615,6 +615,7 @@ "save": "Save", "save_btn": "Save", "search_placeholder": "Search", + "select_language": "Select language", "server_config_blocking_mode_ipv4_faq": "IP address to return for a blocked A request", "server_config_blocking_mode_ipv6_faq": "IP address to return for a blocked AAAA request", "server_config_blocking_mode_ttl_faq": "Specifies how long clients should cache a filtered response", @@ -882,5 +883,6 @@ "yes_disable": "Yes, disable", "yes_disallow": "Yes, disallow", "yes_remove": "Yes, remove", - "yes_reset": "Yes, reset" + "yes_reset": "Yes, reset", + "logs": "Logs" } diff --git a/client_v2/src/__locales/zh-cn.json b/client_v2/src/__locales/zh-cn.json index 4b7df4f6a..67f237223 100644 --- a/client_v2/src/__locales/zh-cn.json +++ b/client_v2/src/__locales/zh-cn.json @@ -17,6 +17,7 @@ "add_blocklist": "添加黑名单", "add_persistent_client": "添加为持久客户端", "add_tls_certificate": "添加 TLS 证书", + "add_tls_certificate_private_key": "添加 TLS 证书私钥", "add_to_allowlist": "添加到白名单", "ads_blocked": "广告已拦截", "ads_blocked_card": "广告已拦截", @@ -36,9 +37,14 @@ "allowlist_remove": "移除允许列表?", "allowlist_remove_desc": "将移除 %value%", "allowlists": "允许列表", + "allowlists_desc": "允许列表中的域名即使同时存在于拦截列表中也不会被拦截", "allowlists_title": "允许列表", "apply": "应用", "aria_clear_input": "清除输入", + "aria_next_page": "下一页", + "aria_previous_page": "上一页", + "auto_clients_desc": "关于可以使用 AdGuard Home 的设备的 IP 地址信息", + "auto_clients_title": "运行时", "autofix_warning_list": "

它将执行以下任务:

停用系统 DNSStubListener

将 DNS 服务器地址设为 127.0.0.1

将 /etc/resolv.conf 的符号链接目标替换为 /run/systemd/resolve/resolv.conf

停止 DNSStubListener(重新加载 systemd-resolved 服务)

", "autofix_warning_result": "因此,默认情况下所有来自系统的 DNS 请求都将由 AdGuard Home 处理。", "autofix_warning_text": "若您单击「修复」,AdGuard Home 将会配置您的系统以使用 AdGuard Home 的 DNS 服务器。", @@ -113,6 +119,7 @@ "client_identifier": "标识符", "client_ip": "客户端 IP", "client_location": "客户端位置", + "client_name_already_exists": "此客户端名称已存在", "client_removed": "客户端已移除", "client_settings": "客户端设置", "client_table_header": "客户端", @@ -140,6 +147,8 @@ "clients_remove_title": "删除客户端?", "clients_tags": "标签", "clients_tags_desc": "您可以为此客户端添加标签,并将其包含在过滤规则中。了解更多", + "clients_tags_placeholder": "添加标签", + "clients_title": "持久客户端", "clients_upstreams_desc": "上游 DNS 服务器,每行一个。", "clients_use_dns_cache": "使用 DNS 缓存", "clients_use_global_settings": "使用全局设置", @@ -165,21 +174,30 @@ "delete_table_action": "删除", "delete_table_action_confirm": "确认删除", "device_type": "设备类型", + "dhcp": "DHCP", "dhcp_add_static_lease": "添加静态租约", "dhcp_config_saved": "已成功保存 DHCP 服务器配置", + "dhcp_dynamic_ip_found": "您的系统对接口 %interface_name% 使用了动态 IP 地址配置。要使用 DHCP 服务器,必须设置静态 IP 地址。您当前的 IP 地址是 %ip%。如果您按下「启用 DHCP 服务器」按钮,AdGuard Home 将自动将此 IP 设为静态地址。", "dhcp_edit_static_lease": "编辑静态租约", + "dhcp_enable": "使用 AdGuard 内置的 DHCP 服务器", "dhcp_error": "检查 DHCP 服务器失败。请稍后重试。", "dhcp_form_gateway_address": "网关 IP 地址", + "dhcp_form_gateway_address_value": "网关 IP 地址:%value%", "dhcp_form_gateway_input": "网关 IP", + "dhcp_form_lease_title": "DHCP 租约时间,单位:秒", "dhcp_form_range_start": "起始 IP 地址", "dhcp_form_range_title": "IP 地址范围", "dhcp_form_subnet_input": "子网掩码", "dhcp_found": "无法启用 DHCP 服务器", + "dhcp_hardware_address_value": "硬件地址:%value%", + "dhcp_interface_select": "DHCP 接口", + "dhcp_ip_addresses_value": "IP 地址:%value%", "dhcp_ipv4_settings": "DHCP IPv4 设置", "dhcp_ipv6_settings": "DHCP IPv6 设置", "dhcp_lease_added": "静态租约「%key%」已成功添加", "dhcp_lease_deleted": "静态租约「%key%」已成功删除", "dhcp_lease_updated": "静态租约「%key%」已成功更新", + "dhcp_leases": "动态租约", "dhcp_leases_not_found": "未找到 DHCP 租约", "dhcp_leases_title": "DHCP 租约", "dhcp_mac_address_already_added": "此 MAC 地址已添加", @@ -191,66 +209,117 @@ "dhcp_reset_leases_success": "成功重置了 DHCP 租约", "dhcp_settings": "DHCP 设置", "dhcp_static_ip_error": "DHCP 服务器需要静态 IP 地址。我们无法确认此网络接口已配置静态 IP。", + "dhcp_static_leases": "静态租约", "dhcp_static_leases_not_found": "未找到 DHCP 静态租约", "dhcp_table_hostname": "主机名", "dhcp_table_ip_address": "IP 地址", "dhcp_table_mac_address": "MAC 地址", + "dhcp_v6_unavailable": "不可用", + "dhcp_warning": "要启用此 DHCP 服务器,请停用网络上所有其他 DHCP 服务器。否则,设备可能会失去互联网连接。", "disable": "禁用", + "disable_dns_rewrites": "禁用 DNS 重写?", "disable_protection_btn": "禁用保护", "disabled_dhcp": "DHCP 服务器已禁用", "disallow_client_confirm_allowlist_note": "此操作还将从允许的客户端列表中移除规则 %ip%。", + "disallow_client_confirm_text": "这将丢弃来自客户端 %ip% 的所有后续 DNS 请求,并将其从允许的客户端列表中移除。", "disallow_client_confirm_title": "禁止此客户端?", "disallow_this_client": "不允许这个客户端", "dns_access_settings_title": "访问设置", "dns_allowed_clients": "允许的客户端", + "dns_allowed_clients_desc": "仅接受此列表中客户端的请求", + "dns_allowed_clients_desc_2": "要添加客户端,请输入其 CIDR、IP 地址或 ClientID", "dns_allowed_clients_label": "允许的客户端,每行一个", "dns_allowed_clients_placeholder": "输入允许的客户端", "dns_blocking_mode": "拦截模式", "dns_blocking_mode_custom_ip": "自定义 IP 地址", + "dns_blocking_mode_custom_ip_desc": "以手动设置的 IP 地址响应", "dns_blocking_mode_default": "默认", + "dns_blocking_mode_default_desc": "如果被 Adblock 风格规则拦截,对 A 记录响应 0.0.0.0,对 AAAA 记录响应 ::。如果被 hosts 风格规则拦截,则响应规则中指定的 IP 地址。", + "dns_blocking_mode_desc": "设置对被拦截请求的响应类型", "dns_blocking_mode_ipv4_label": "拦截 IPv4", "dns_blocking_mode_ipv4_placeholder": "输入 IPv4 地址", "dns_blocking_mode_ipv6_label": "拦截 IPv6", "dns_blocking_mode_ipv6_placeholder": "输入 IPv6 地址", "dns_blocking_mode_null_ip": "空 IP 地址", + "dns_blocking_mode_null_ip_desc": "对 A 记录响应 0.0.0.0,对 AAAA 记录响应 ::", "dns_blocking_mode_nxdomain": "NXDOMAIN", + "dns_blocking_mode_nxdomain_desc": "以 NXDOMAIN 状态码响应", "dns_blocking_mode_refused": "REFUSED", + "dns_blocking_mode_refused_desc": "以 REFUSED 状态码响应", "dns_blocking_mode_title": "拦截模式", "dns_blocking_mode_ttl_label": "拦截响应 TTL(秒)", + "dns_blocking_mode_ttl_placeholder": "输入 TTL", + "dns_bootstrap_dns_desc": "设置用于解析 DoH/DoT 上游解析器主机名的 DNS 服务器", + "dns_bootstrap_dns_desc_2": "每行输入一个 IP 地址。注释必须以 # 开头且另起一行。", "dns_bootstrap_dns_label": "Bootstrap DNS 服务器,每行一个", "dns_bootstrap_dns_placeholder": "IP 地址", "dns_bootstrap_dns_title": "Bootstrap DNS 服务器", "dns_bootstrap_servers": "Bootstrap DNS 服务器", "dns_cache_config": "DNS 缓存配置", "dns_cache_configuration_saved_toast": "DNS 缓存配置已保存", + "dns_cache_desc": "在本地存储 DNS 响应", "dns_cache_size": "缓存大小", + "dns_cache_size_desc": "设置 DNS 缓存大小", "dns_cache_size_label": "DNS 缓存大小,单位:字节", "dns_cache_size_title": "缓存大小", + "dns_cache_title": "DNS 缓存", "dns_clear_cache": "清除缓存", "dns_clear_cache_cancel": "取消", "dns_clear_cache_confirm": "是的,清除", + "dns_clear_cache_desc": "这将清除您的设备 DNS 缓存,并使其从 DNS 服务器请求新的信息", + "dns_clear_cache_title": "清除缓存?", "dns_clear_cache_yes": "是的,清除", "dns_config": "DNS 服务配置", "dns_disallowed_clients": "不允许的客户端", + "dns_disallowed_clients_desc": "丢弃此列表中客户端的请求。当「允许的客户端」中有条目时,此设置将被忽略。", "dns_disallowed_clients_label": "禁止的客户端,每行一个", "dns_disallowed_clients_title": "不允许的客户端", "dns_disallowed_domains": "不允许的域名", + "dns_disallowed_domains_desc": "丢弃匹配这些域名的 DNS 查询,且这些查询不会出现在查询日志中。", + "dns_disallowed_domains_desc_2": "输入精确域名、通配符或 URL 过滤规则,例如 example.org、*.example.org 或 ||example.org^", "dns_disallowed_domains_label": "禁止的域名,每行一个", "dns_disallowed_domains_title": "不允许的域名", + "dns_dnssec": "DNSSEC", + "dns_dnssec_desc": "在出站 DNS 查询中添加 DNSSEC 标志并检查结果。需要支持 DNSSEC 的解析器。", + "dns_edns_client_subnet": "EDNS 客户端子网", + "dns_edns_client_subnet_desc": "向上游请求添加 EDNS 客户端子网(ECS)数据,并在查询日志中记录 ECS 值。", + "dns_edns_custom_label": "EDNS 自定义 IP 地址", "dns_edns_custom_placeholder": "输入 IP 地址", + "dns_edns_desc": "向上游请求添加 EDNS 客户端子网(ECS)数据,并在查询日志中记录 ECS 值。您可以使用默认的客户端子网或为 ECS 指定自定义 IP 地址。", + "dns_edns_option_custom": "使用自定义 IP 地址", + "dns_edns_option_default": "使用默认 EDNS 客户端子网", + "dns_edns_title": "EDNS 客户端子网", + "dns_fallback_dns_desc": "设置当上游 DNS 服务器不可用时使用的备用 DNS 服务器", + "dns_fallback_dns_desc_2": "使用与 上游 DNS 服务器 相同的语法", "dns_fallback_dns_label": "备用 DNS 服务器,每行一个", "dns_fallback_dns_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://", "dns_fallback_dns_title": "后备 DNS 服务器", "dns_fallback_servers": "后备 DNS 服务器", + "dns_ipv6_resolution": "IPv6 解析", + "dns_ipv6_resolution_desc": "启用 IPv6 地址(AAAA 记录)的 DNS 查询解析,并向 HTTPS 响应中添加 IPv6 提示。", "dns_optimistic_caching": "乐观缓存", + "dns_optimistic_caching_desc": "即使缓存条目已过期,也从缓存中响应并尝试更新它们。", "dns_over_https": "DNS-over-HTTPS", "dns_over_quic": "DNS-over-QUIC", "dns_over_tls": "DNS-over-TLS", "dns_override_max_ttl": "覆盖最大 TTL 值", + "dns_override_max_ttl_desc": "设置 DNS 响应缓存的最大时间。大于此值的 TTL 将被降低到此值。", + "dns_override_max_ttl_label": "最大生存时间(TTL),单位:秒", + "dns_override_max_ttl_placeholder": "生存时间(TTL)", "dns_override_max_ttl_title": "覆盖最大 TTL 值", "dns_override_min_ttl": "覆盖最小 TTL 值", + "dns_override_min_ttl_desc": "设置 DNS 响应缓存的最小时间。小于此值的 TTL 将被提高到此值。", + "dns_override_min_ttl_label": "最小生存时间(TTL),单位:秒", + "dns_override_min_ttl_placeholder": "生存时间(TTL)", "dns_override_min_ttl_title": "覆盖最小 TTL 值", "dns_privacy": "DNS 隐私", + "dns_private_reverse_resolve_clients_desc": "通过 PTR 查询将客户端 IP 地址解析为主机名。本地客户端使用私有 DNS 服务器,公网 IP 客户端使用上游服务器。", + "dns_private_reverse_resolve_clients_title": "客户端 IP 地址反向查询", + "dns_private_reverse_resolvers": "私有反向 DNS 解析器", + "dns_private_reverse_resolvers_desc": "通过私有上游服务器、DHCP 和 /etc/hosts 解析包含私有 IP 地址的 ARPA 域名的 PTR、SOA 和 NS 请求。", + "dns_private_reverse_resolvers_disabled_desc": "禁用后,对所有此类请求响应 NXDOMAIN。", + "dns_private_reverse_servers_desc": "设置用于私有 PTR、SOA 和 NS 请求的 DNS 服务器。这些请求来自具有私有 IP 地址的客户端,查询的 ARPA 域名包含私有 IP 地址范围内的子网。", + "dns_private_reverse_servers_desc_2": "如果此字段为空,将使用操作系统的默认 DNS 解析器(AdGuard Home IP 地址除外)。默认情况下,AdGuard Home 使用以下反向 DNS 解析器:%value_1% 和 %value_2%。", "dns_private_reverse_servers_label": "私有反向 DNS 服务器,每行一个", "dns_private_reverse_servers_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://", "dns_private_reverse_servers_title": "私人反向 DNS 服务器", @@ -260,12 +329,25 @@ "dns_query": "DNS 查询", "dns_rate_limit": "速度限制", "dns_rate_limit_allowlist": "速率限制白名单", + "dns_rate_limit_allowlist_desc": "指定不受速率限制的 IP 地址", + "dns_rate_limit_allowlist_label": "IP 地址,每行一个", "dns_rate_limit_allowlist_placeholder": "IP 地址", "dns_rate_limit_allowlist_title": "速率限制白名单", + "dns_rate_limit_desc": "设置每个客户端每秒允许的请求数", + "dns_rate_limit_desc_2": "值为 0 表示无限制", + "dns_rate_limit_no_limit": "无限制", "dns_rate_limit_placeholder": "输入限制速率", "dns_rate_limit_title": "速度限制", + "dns_rate_limit_value": "%value% 请求/秒", + "dns_rewrite_exists": "此 DNS 重写已存在", + "dns_rewrite_removed": "DNS 重写已移除", + "dns_rewrite_same": "不能重写到相同的域名或通配符", "dns_rewrites": "DNS 重写", + "dns_rewrites_desc": "列出您为特定域名设置的自定义 DNS 响应", "dns_server_addresses": "服务器地址", + "dns_server_addresses_configured_in_file": "服务器地址已在文件 %path% 中配置", + "dns_server_addresses_desc": "设置 AdGuard Home 可通过哪些 DNS 服务器地址访问", + "dns_server_addresses_desc_2": "查看 配置上游 DNS 服务器的提示 以及我们的 已知 DNS 提供商列表", "dns_server_addresses_label": "服务器地址(每行一个)", "dns_server_addresses_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://", "dns_server_addresses_title": "服务器地址", @@ -273,9 +355,22 @@ "dns_server_configuration_saved_toast": "DNS 服务器配置已保存", "dns_settings": "DNS", "dns_subnet_placeholder": "输入前缀长度", + "dns_subnet_prefix": "%value% 地址的子网前缀长度", + "dns_subnet_prefix_desc": "设置用于速率限制的 %value% 地址的子网前缀长度", + "dns_subnet_prefix_title": "%value% 地址的子网前缀长度", + "dns_test_not_ok_toast": "上游 %key% 通过了语法检查但测试失败。请验证上游设置。", + "dns_test_ok_toast": "所有上游服务器均正常运行", + "dns_test_parsing_error_toast": "无法从 %section% 部分的第 %number% 行提取上游名称。请检查行格式。", "dns_test_upstreams": "测试上游", + "dns_test_warning_toast": "上游 %key% 未响应 DNS 查询。请验证上游设置。", + "dns_ttl_value": "%value% 秒", + "dns_upstream_mode": "上游模式", + "dns_upstream_mode_desc": "决定如何将网络的 DNS 查询路由到多个上游服务器", + "dns_upstream_mode_title": "上游模式", "dns_upstream_servers_title": "上游 DNS 服务器", "dns_upstream_timeout": "上游超时", + "dns_upstream_timeout_desc": "设置等待上游服务器响应的时长", + "dns_upstream_timeout_placeholder": "输入超时时间(秒)", "dns_upstream_timeout_title": "上游超时", "dns_upstream_validation_invalid": "格式无效", "domain": "域名", @@ -284,6 +379,7 @@ "dynamic_dhcp_leases_not_found": "未找到动态 DHCP 租约", "edit_table_action": "编辑", "enable": "启用", + "enable_dns_rewrites": "启用 DNS 重写?", "enabled_dhcp": "DHCP 服务器已启用", "encrypted_dns_addresses": "加密 DNS 服务器地址", "encrypted_dns_settings": "加密 DNS 服务器设置", @@ -315,8 +411,10 @@ "encryption_https": "HTTPS 端口", "encryption_https_summary": "HTTPS 端口:%value%", "encryption_https_tooltip": "此端口提供 AdGuard Home 管理界面的 HTTPS 访问。同时在 '/dns-query' 路径上提供 DNS-over-HTTPS。", + "encryption_invalid_data": "数据无效", "encryption_issuer": "颁发者:%value%", "encryption_key": "私钥", + "encryption_key_cert_mismatch": "私钥与证书不匹配", "encryption_key_input": "将您以 PEM 格式编码的证书私钥复制粘贴到此处。", "encryption_key_invalid": "私钥无效", "encryption_key_source_content": "粘贴私钥内容", @@ -324,6 +422,7 @@ "encryption_key_type": "加密算法:%value%", "encryption_key_valid": "私钥有效", "encryption_plain_dns": "无加密 DNS", + "encryption_plain_dns_desc": "纯 DNS 默认启用。禁用它将强制所有设备使用加密 DNS。要禁用纯 DNS,请至少指定一个加密 DNS 协议。", "encryption_plain_dns_error": "要禁用无加密 DNS,请至少启用一个加密 DNS 协议", "encryption_private_key_path": "私钥路径", "encryption_server": "服务器名称", @@ -333,6 +432,8 @@ "encryption_server_tooltip_2": "必须与证书中的 DNS 名称匹配", "encryption_subject": "主题:%value%", "encryption_title": "加密", + "encryption_unable_read_cert": "无法读取证书文件", + "encryption_unable_read_key": "无法读取私钥文件", "enter_cache_size": "输入缓存大小(字节)", "enter_ip_address_placeholder": "输入 IP 地址", "error": "错误", @@ -368,12 +469,14 @@ "form_error_domain_format": "无效的域格式", "form_error_equal": "不可相同", "form_error_format": "格式无效", + "form_error_format_line": "第 %line% 行格式无效", + "form_error_format_lines": "第 %lines% 行格式无效", "form_error_gateway_ip": "租约期限不能有网关的 IP 地址", "form_error_hostname_format": "请使用数字、小写英文字母或连字符。", - "form_error_ip4_format": "无效的 IPv4 地址", - "form_error_ip6_format": "无效的 IPv6 地址", "form_error_ip_already_added": "此 IP 地址已添加", "form_error_ip_format": "无效 IP 地址", + "form_error_ip4_format": "无效的 IPv4 地址", + "form_error_ip6_format": "无效的 IPv6 地址", "form_error_mac_already_added": "此 MAC 地址已添加", "form_error_mac_format": "无效的 MAC 地址", "form_error_password": "密码不匹配", @@ -381,12 +484,17 @@ "form_error_port_range": "输入 80 - 65535 范围内的端口值", "form_error_port_unsafe": "这是一个不安全的端口", "form_error_range": "数值必须介于 %min% 和 %max% 之间", + "form_error_required": "请填写此字段", "form_error_server_name": "无效的服务器名", + "form_error_subnet": "IP 地址应在子网 %cidr% 中", "form_error_url_format": "输入一个有效的 URL 或文件路径", + "form_value_value_from_error": "输入介于 %min_value% 和 %max_value% 之间的值", + "form_value_value_min_error": "输入 %min_value% 或更大的值", "found_in_known_domains": "在已知域名中找到", "friday": "星期五", "gateway_or_subnet_invalid": "子网掩码无效", "general_statistics": "概况统计", + "greater_range_start_error": "范围的结束地址必须大于起始地址", "home_dns_addresses": "AdGuard Home DNS 地址", "home_dns_addresses_desc": "AdGuard Home DNS 服务器监听以下接口/端口:", "ignore_domains_desc_log": "匹配这些规则的请求不会显示在「查询日志」中", @@ -430,6 +538,7 @@ "light_theme": "浅色主题", "list_updated": "%count% 个列表已更新 | %count% 个列表已更新", "login": "登录", + "logs": "日志", "logout": "退出", "make_static": "静态化", "make_static_desc": "此操作将动态地址转换为静态地址", @@ -448,6 +557,7 @@ "notify_undo": "撤销", "notify_updated": "已更新", "open_dashboard": "打开仪表盘", + "out_of_range_error": "地址不能在 DHCP 范围 %start%–%end% 内", "parental_control": "家长控制", "parental_group_ai": "人工智能", "parental_group_cdn": "内容分发网络", @@ -465,6 +575,7 @@ "parental_group_software": "软件开发", "parental_group_streaming": "串流", "password_label": "密码", + "password_login_error": "用户名和密码不匹配", "password_placeholder": "输入密码", "password_requirements": "密码要求", "password_requirements_characters": "至少 8 个字符", @@ -472,6 +583,7 @@ "password_requirements_match": "密码匹配", "password_requirements_special": "英文字母、数字和特殊字符", "password_requirements_uppercase": "至少 1 个大写字母", + "path_to_file_placeholder": "文件路径:本地或网络地址", "pause_for_hour": "暂停 %count% 小时", "pause_for_minutes": "暂停 %count% 分钟 | 暂停 %count% 分钟", "pause_for_seconds": "暂停 %count% 秒 | 暂停 %count% 秒", @@ -487,6 +599,7 @@ "protocol": "协议", "protocols": "协议", "queries": "查询", + "queries_tooltip": "%value% 条查询", "queries_total": "共 %value% 条查询 | 共 %value% 条查询", "query_details": "查询详情", "query_log": "查询日志", @@ -522,6 +635,7 @@ "query_log_detail_time": "时间:%value%", "query_log_detail_type": "类型:%value%", "query_log_nothing_available": "没找到", + "query_log_nothing_available_rotation": "查询日志已在「常规设置」中禁用", "query_log_processed": "已处理", "query_log_retention": "日志轮替", "query_log_rewritten": "重写项", @@ -539,6 +653,8 @@ "request_table_header": "请求", "requests_table_header": "请求", "reset": "重置", + "reset_dhcp_settings": "重置 DHCP 设置", + "reset_dns_confirm_text": "所有 DNS 协议设置将重置为默认值", "reset_dns_confirm_title": "重置 DNS 协议?", "reset_dns_protocols": "重置 DNS 协议", "reset_settings": "重置设置", @@ -548,6 +664,7 @@ "result": "结果", "resume_protection_timer": "防护将在 %time% 恢复", "rewrite_add": "添加 DNS 重写", + "rewrite_domain": "域名或通配符", "rewrite_domain_input_placeholder": "输入域名或通配符", "rewrite_edit": "编辑 DNS 重写", "rewrite_hosts_applied": "根据 hosts 文件规则已被重写", @@ -574,6 +691,7 @@ "save": "保存", "save_btn": "保存", "search_placeholder": "搜索", + "select_language": "选择语言", "selected": "已选择 %value%", "server_config_blocking_mode": "拦截模式", "server_config_blocking_mode_custom_ip_desc": "自定义 IP 地址:响应手动设置的 IP 地址", @@ -613,6 +731,7 @@ "server_config_subnet_len_ipv6_faq": "用于速率限制的 IPv6 地址子网前缀长度。默认为 56", "server_config_subnet_len_placeholder": "输入前缀长度", "set_static_ip": "设置一个静态 IP", + "set_static_ip_manually": "设置静态 IP 地址", "settings": "设置", "settings_anonymize_client_ip": "对客户端 IP 地址进行匿名化处理", "settings_anonymize_client_ip_desc": "将 IP 地址的一部分替换为零", @@ -632,7 +751,7 @@ "settings_filter_requests": "过滤请求", "settings_filter_requests_desc": "您可以使用拦截列表允许列表用户规则来设置过滤规则。", "settings_filtering_and_security": "过滤与安全", - "settings_general_short": "General", + "settings_general_short": "常规", "settings_global": "全局", "settings_hours": "%count% 个小时 | %count% 个小时", "settings_log_dns_requests": "记录 DNS 请求", @@ -736,6 +855,7 @@ "stats_adult": "被拦截的成人网站", "stats_query_domain": "请求域名排行", "status_table_header": "状态", + "subnet_error": "范围地址必须在同一子网内", "sunday": "星期日", "system_host_files": "系统主机文件", "system_theme": "系统主题", @@ -746,7 +866,14 @@ "threats_blocked_tooltip": "被 AdGuard 浏览安全模块拦截的 DNS 查询数量", "thursday": "星期四", "time_table_header": "时间", + "tls_cert_modal_description": "要使用加密,您必须为您的域名提供有效的 TLS 证书链。您可以从 letsencrypt.org 获取免费 TLS 证书,或从受信任的证书颁发机构购买。", + "tls_cert_path_label": "证书文件的完整路径", + "tls_cert_path_option": "输入证书文件的完整路径", "tls_certificate": "TLS 证书", + "tls_certificate_expired": "您的 TLS 证书已过期", + "tls_certificate_expiring": "您的 TLS 证书即将过期", + "tls_key_path_label": "私钥文件的完整路径", + "tls_key_path_option": "输入私钥文件的完整路径", "top_blocked_domains": "被拦截域名排行", "top_clients": "客户端排行", "top_upstreams": "经常请求的上游服务器", @@ -759,14 +886,18 @@ "unblock": "取消拦截", "unblock_client": "解除拦截客户端", "unknown_filter": "未知过滤器 %filterId%", + "update_available": "版本 %version% 可用。发布说明", "update_button": "更新", + "update_failed": "自动更新失败。请 按此步骤 手动更新。", "update_filters_custom_hours": "自定义间隔(小时)", + "update_filters_desc": "设置过滤器的自动更新间隔。适用于通过外部链接添加的拦截列表和允许列表。", "update_filters_disable": "禁用", "update_filters_interval_custom": "用户", "update_filters_interval_daily": "每天", "update_filters_interval_hourly": "每小时", "update_filters_interval_weekly": "每周", "update_filters_title": "自动更新过滤器", + "update_how_to": "如何更新", "updated_upstream_dns_toast": "上游服务器已保存", "updates_checked": "AdGuard Home 的新版本现在可用", "updates_version_equal": "AdGuard Home已经是最新版本", @@ -778,7 +909,12 @@ "upstream_dns_addresses_faq": "了解更多关于上游 DNS 服务器配置的信息。以下为已知 DNS 提供商列表供您选择。", "upstream_dns_configured_in_file": "配置于 %path%", "upstream_dns_fastest_addr": "最快的 IP 地址", + "upstream_dns_fastest_addr_desc": "等待所有 DNS 服务器响应,测量每个服务器的 TCP 连接速度,并返回连接速度最快的服务器的 IP 地址。", + "upstream_dns_fastest_addr_warning": "如果一个或多个上游服务器没有响应,此模式可能会显著降低 DNS 查询速度。请确保上游服务器稳定且上游超时时间设置较低。", + "upstream_dns_load_balancing": "负载均衡", + "upstream_dns_load_balancing_desc": "

每次查询一个上游服务器

AdGuard Home 使用加权随机算法选择失败查询次数最少且平均查询时间最低的服务器

", "upstream_dns_parallel_requests": "并行请求", + "upstream_dns_parallel_requests_desc": "使用并行查询以加速解析,同时查询所有上游服务器", "upstream_dns_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://", "upstream_dns_servers_title": "上游 DNS 服务器", "upstream_enable_reverse_lookup_desc": "通过向适当的解析器发送 PTR 查询,将客户端 IP 地址反向解析为主机名:本地客户端使用私有 DNS 服务器,公网 IP 地址客户端使用上游服务器。", @@ -824,6 +960,7 @@ "user_rules_check_hostname_placeholder": "输入目标", "user_rules_check_title": "检查全局或针对特定客户端的域名过滤", "user_rules_cname": "CNAME:%cname%", + "user_rules_desc": "每行输入一条规则。使用 Adblock 风格或 hosts 文件语法。", "user_rules_disable_browsing_security": "禁用浏览安全", "user_rules_disable_filter": "禁用过滤器", "user_rules_disable_parental_control": "停用家长控制", @@ -856,6 +993,7 @@ "user_rules_rewrite_rule_is_applied": " 重定向规则已应用", "user_rules_rewritten_to": "已重写为:%value%", "user_rules_rule": "规则:%rule%", + "user_rules_rule_added": "已添加用户规则:%rule%", "user_rules_rule_added_to_custom_filtering_rules": "规则已添加到自定义过滤规则", "user_rules_rule_removed": "规则已从自定义过滤规则中移除", "user_rules_safe_search_disabled": "安全搜索已禁用", @@ -869,9 +1007,12 @@ "username_label": "用户名", "username_placeholder": "输入用户名", "validated_with_dnssec": "通过 DNSSEC 验证", + "version_number": "版本 %value%", "wednesday": "星期三", "whois": "WHOIS", "yes": "是", "yes_disable": "禁用", + "yes_disallow": "是的,禁止", + "yes_remove": "是的,移除", "yes_reset": "重置" } diff --git a/client_v2/src/__tests__/access-store.test.ts b/client_v2/src/__tests__/access-store.test.ts index 259fef3ba..c77cacfb5 100644 --- a/client_v2/src/__tests__/access-store.test.ts +++ b/client_v2/src/__tests__/access-store.test.ts @@ -1,36 +1,34 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - getAccessList: vi.fn(), - setAccessList: vi.fn(), + accessList: vi.fn(), + accessSet: vi.fn(), addSuccessToast: vi.fn(), addErrorToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - getAccessList: mocks.getAccessList, - setAccessList: mocks.setAccessList, - }, +vi.mock('panel/api/generated', () => ({ + accessList: mocks.accessList, + accessSet: mocks.accessSet, })); vi.mock('panel/stores/toasts', () => ({ addSuccessToast: mocks.addSuccessToast, addErrorToast: mocks.addErrorToast, })); -import { toggleClientBlock } from 'panel/stores/access'; +import { toggleClientBlock, setAccessList } from 'panel/stores/access'; describe('toggleClientBlock', () => { beforeEach(() => vi.clearAllMocks()); it('not-disallowed + allowlist mode with >1 allowed → removes from allowed', async () => { - mocks.getAccessList.mockResolvedValue({ + mocks.accessList.mockResolvedValue({ allowed_clients: ['1.1.1.1', '2.2.2.2'], disallowed_clients: [], blocked_hosts: [], }); await toggleClientBlock('1.1.1.1', false, ''); - expect(mocks.setAccessList).toHaveBeenCalledWith({ + expect(mocks.accessSet).toHaveBeenCalledWith({ allowed_clients: ['2.2.2.2'], disallowed_clients: [], blocked_hosts: [], @@ -38,13 +36,13 @@ describe('toggleClientBlock', () => { }); it('not-disallowed, no allowlist → adds to disallowed', async () => { - mocks.getAccessList.mockResolvedValue({ + mocks.accessList.mockResolvedValue({ allowed_clients: [], disallowed_clients: [], blocked_hosts: [], }); await toggleClientBlock('3.3.3.3', false, ''); - expect(mocks.setAccessList).toHaveBeenCalledWith({ + expect(mocks.accessSet).toHaveBeenCalledWith({ allowed_clients: [], disallowed_clients: ['3.3.3.3'], blocked_hosts: [], @@ -52,13 +50,13 @@ describe('toggleClientBlock', () => { }); it('disallowed + allowlist mode → adds to allowed', async () => { - mocks.getAccessList.mockResolvedValue({ + mocks.accessList.mockResolvedValue({ allowed_clients: ['1.1.1.1'], disallowed_clients: ['2.2.2.2'], blocked_hosts: [], }); await toggleClientBlock('2.2.2.2', true, ''); - expect(mocks.setAccessList).toHaveBeenCalledWith({ + expect(mocks.accessSet).toHaveBeenCalledWith({ allowed_clients: ['1.1.1.1', '2.2.2.2'], disallowed_clients: ['2.2.2.2'], blocked_hosts: [], @@ -66,16 +64,51 @@ describe('toggleClientBlock', () => { }); it('disallowed, no allowlist → removes from disallowed (uses rule)', async () => { - mocks.getAccessList.mockResolvedValue({ + mocks.accessList.mockResolvedValue({ allowed_clients: [], disallowed_clients: ['client:X'], blocked_hosts: [], }); await toggleClientBlock('1.2.3.4', true, 'client:X'); - expect(mocks.setAccessList).toHaveBeenCalledWith({ + expect(mocks.accessSet).toHaveBeenCalledWith({ allowed_clients: [], disallowed_clients: [], blocked_hosts: [], }); }); }); + +describe('setAccessList', () => { + beforeEach(() => vi.clearAllMocks()); + + it('splits newline-delimited strings into arrays', async () => { + await setAccessList({ + allowed_clients: '1.1.1.1\n2.2.2.2', + disallowed_clients: '3.3.3.3', + blocked_hosts: 'badhost.com\nevil.com', + }); + expect(mocks.accessSet).toHaveBeenCalledWith({ + allowed_clients: ['1.1.1.1', '2.2.2.2'], + disallowed_clients: ['3.3.3.3'], + blocked_hosts: ['badhost.com', 'evil.com'], + }); + }); + + it('handles single values correctly', async () => { + await setAccessList({ allowed_clients: '1.1.1.1' }); + expect(mocks.accessSet).toHaveBeenCalledWith({ + allowed_clients: ['1.1.1.1'], + disallowed_clients: undefined, + blocked_hosts: undefined, + }); + }); + + it('handles empty values', async () => { + await setAccessList({ allowed_clients: '' }); + expect(mocks.accessSet).toHaveBeenCalledWith({ + allowed_clients: [], + disallowed_clients: undefined, + blocked_hosts: undefined, + }); + }); +}); diff --git a/client_v2/src/__tests__/app-routing.test.tsx b/client_v2/src/__tests__/app-routing.test.tsx index e1ae84d9d..04829ca8c 100644 --- a/client_v2/src/__tests__/app-routing.test.tsx +++ b/client_v2/src/__tests__/app-routing.test.tsx @@ -45,7 +45,7 @@ vi.mock('panel/common/ui/Footer', () => ({ Footer: () =>
, })); vi.mock('panel/common/ui/Icons', () => ({ Icons: (): null => null })); -vi.mock('panel/components/Toasts', () => ({ default: (): null => null })); +vi.mock('panel/components/Toasts', () => ({ Toasts: (): null => null })); // Deterministic marker so the assertion does not depend on Dashboard data/i18n. vi.mock('panel/components/Dashboard', () => ({ diff --git a/client_v2/src/__tests__/clientForm/store.test.ts b/client_v2/src/__tests__/clientForm/store.test.ts index 0dac70f28..ea2ee1a3f 100644 --- a/client_v2/src/__tests__/clientForm/store.test.ts +++ b/client_v2/src/__tests__/clientForm/store.test.ts @@ -72,6 +72,7 @@ describe('clientForm store', () => { duckduckgo: false, yandex: false, pixabay: false, + ecosia: false, }, }); expect(clientFormState.safe_search.enabled).toBe(true); diff --git a/client_v2/src/__tests__/common/intl/locales.generated.spec.ts b/client_v2/src/__tests__/common/intl/locales.generated.spec.ts new file mode 100644 index 000000000..c922affba --- /dev/null +++ b/client_v2/src/__tests__/common/intl/locales.generated.spec.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; + +import { LOCALES, LOCALE_LOADERS, LOCALE_CODES } from 'panel/common/intl/locales.generated'; +import twosky from 'Twosky'; + +const homeV2 = twosky.find((p) => p.project_id === 'home_v2')!; + +describe('locales.generated.ts', () => { + const allTwoskyCodes = Object.keys(homeV2.languages); + const expectedCodes = allTwoskyCodes.sort(); + + it('LOCALE_CODES lists every home_v2 language', () => { + expect([...LOCALE_CODES].sort()).toEqual(expectedCodes); + }); + + it('LOCALE_LOADERS has a loader for every non-en locale', () => { + const loaderCodes = Object.keys(LOCALE_LOADERS).sort(); + expect(loaderCodes).toEqual(expectedCodes.filter((c) => c !== 'en')); + }); + + it('LOCALES only contains the base locale (en) initially', () => { + expect(Object.keys(LOCALES)).toEqual(['en']); + }); + + it('base locale (en) is present and non-empty', () => { + expect(LOCALES.en).toBeDefined(); + expect(Object.keys(LOCALES.en).length).toBeGreaterThan(0); + }); + + it('every lazy loader resolves to a non-empty message map', async () => { + for (const [code, loader] of Object.entries(LOCALE_LOADERS)) { + const mod = await loader(); + const messages = (mod as { default?: Record }).default ?? mod; + expect(messages, `locale ${code}`).toBeDefined(); + expect( + Object.keys(messages as Record).length, + `locale ${code}`, + ).toBeGreaterThan(0); + } + }); +}); diff --git a/client_v2/src/__tests__/dashboard-store.test.ts b/client_v2/src/__tests__/dashboard-store.test.ts index bc89db1f5..99664b4df 100644 --- a/client_v2/src/__tests__/dashboard-store.test.ts +++ b/client_v2/src/__tests__/dashboard-store.test.ts @@ -1,22 +1,21 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - getTlsStatus: vi.fn(), - getGlobalVersion: vi.fn(), + tlsStatus: vi.fn(), + getVersionJson: vi.fn(), getProfile: vi.fn(), addErrorToast: vi.fn(), addSuccessToast: vi.fn(), addNoticeToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - baseUrl: 'http://x', - getGlobalVersion: mocks.getGlobalVersion, - getProfile: mocks.getProfile, - getUpdate: vi.fn(), - getTlsStatus: mocks.getTlsStatus, - }, +vi.mock('panel/api/generated', () => ({ + baseUrl: 'http://x', + getStatusUrl: () => 'control/status', + getVersionJson: mocks.getVersionJson, + getProfile: mocks.getProfile, + beginUpdate: vi.fn(), + tlsStatus: mocks.tlsStatus, })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast, @@ -30,12 +29,12 @@ describe('getDnsStatus', () => { beforeEach(() => vi.clearAllMocks()); it('fetches TLS status when the core is running', async () => { - mocks.getGlobalVersion.mockResolvedValue({ + mocks.getVersionJson.mockResolvedValue({ disabled: true, new_version: 'x', }); mocks.getProfile.mockResolvedValue({ name: 'n', theme: 't' }); - mocks.getTlsStatus.mockResolvedValue({ enabled: false }); + mocks.tlsStatus.mockResolvedValue({ enabled: false }); vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response( @@ -54,6 +53,6 @@ describe('getDnsStatus', () => { await getDnsStatus(); // allow microtasks for chained getVersion/getTlsStatus/getProfile await new Promise((r) => setTimeout(r, 0)); - expect(mocks.getTlsStatus).toHaveBeenCalled(); + expect(mocks.tlsStatus).toHaveBeenCalled(); }); }); diff --git a/client_v2/src/__tests__/dhcp-store.test.ts b/client_v2/src/__tests__/dhcp-store.test.ts index ea9c21601..c1c61fb40 100644 --- a/client_v2/src/__tests__/dhcp-store.test.ts +++ b/client_v2/src/__tests__/dhcp-store.test.ts @@ -1,22 +1,20 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - findActiveDhcp: vi.fn(), - getDhcpInterfaces: vi.fn(), - getDhcpStatus: vi.fn(), - setDhcpConfig: vi.fn(), + checkActiveDhcp: vi.fn(), + dhcpInterfaces: vi.fn(), + dhcpStatus: vi.fn(), + dhcpSetConfig: vi.fn(), addErrorToast: vi.fn(), addSuccessToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - findActiveDhcp: mocks.findActiveDhcp, - getDhcpInterfaces: mocks.getDhcpInterfaces, - getDhcpStatus: mocks.getDhcpStatus, - getGlobalStatus: vi.fn(), - setDhcpConfig: mocks.setDhcpConfig, - }, +vi.mock('panel/api/generated', () => ({ + checkActiveDhcp: mocks.checkActiveDhcp, + dhcpInterfaces: mocks.dhcpInterfaces, + dhcpStatus: mocks.dhcpStatus, + status: vi.fn(), + dhcpSetConfig: mocks.dhcpSetConfig, })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast, @@ -29,19 +27,19 @@ describe('findActiveDhcp', () => { beforeEach(() => vi.clearAllMocks()); it('passes { interface } not a bare string', async () => { - mocks.findActiveDhcp.mockResolvedValue({ + mocks.checkActiveDhcp.mockResolvedValue({ v4: { other_server: { found: 'yes' }, static_ip: { static: 'yes' } }, v6: { other_server: {} }, }); await findActiveDhcp('eth0'); - expect(mocks.findActiveDhcp).toHaveBeenCalledWith({ interface: 'eth0' }); + expect(mocks.checkActiveDhcp).toHaveBeenCalledWith({ interface: 'eth0' }); }); it('shows dhcp_found error with retry action when another DHCP server detected', async () => { - mocks.getDhcpInterfaces.mockResolvedValue({ + mocks.dhcpInterfaces.mockResolvedValue({ eth0: { ipv4_addresses: ['1.1.1.1'], ipv6_addresses: [] }, }); - mocks.findActiveDhcp.mockResolvedValue({ + mocks.checkActiveDhcp.mockResolvedValue({ v4: { other_server: { found: 'yes' }, static_ip: { static: 'yes', ip: 'x' }, @@ -58,10 +56,10 @@ describe('findActiveDhcp', () => { }); it('shows dhcp_not_found success toast when clean', async () => { - mocks.getDhcpInterfaces.mockResolvedValue({ + mocks.dhcpInterfaces.mockResolvedValue({ eth0: { ipv4_addresses: ['1.1.1.1'], ipv6_addresses: [] }, }); - mocks.findActiveDhcp.mockResolvedValue({ + mocks.checkActiveDhcp.mockResolvedValue({ v4: { other_server: { found: 'no' }, static_ip: { static: 'yes', ip: '1.1.1.1' }, @@ -77,12 +75,18 @@ describe('findActiveDhcp', () => { describe('setDhcpConfig', () => { beforeEach(() => { vi.clearAllMocks(); - mocks.setDhcpConfig.mockResolvedValue(undefined); + mocks.dhcpSetConfig.mockResolvedValue(undefined); }); it('shows dhcp_config_saved toast', async () => { await setDhcpConfig({ - v4: { range_start: 'a', range_end: 'b' }, + v4: { + gateway_ip: '192.168.1.1', + subnet_mask: '255.255.255.0', + range_start: '192.168.1.100', + range_end: '192.168.1.200', + lease_duration: 86400, + }, interface_name: 'eth0', }); expect(mocks.addSuccessToast).toHaveBeenCalled(); @@ -92,12 +96,12 @@ describe('setDhcpConfig', () => { describe('toggleDhcp', () => { beforeEach(() => { vi.clearAllMocks(); - mocks.setDhcpConfig.mockResolvedValue(undefined); + mocks.dhcpSetConfig.mockResolvedValue(undefined); }); it('computes enabled from passed config, not current state', async () => { await toggleDhcp({ enabled: false, interface_name: 'eth0' }); - expect(mocks.setDhcpConfig).toHaveBeenCalledWith( + expect(mocks.dhcpSetConfig).toHaveBeenCalledWith( expect.objectContaining({ enabled: true }), ); }); diff --git a/client_v2/src/__tests__/dhcp-toggle.test.tsx b/client_v2/src/__tests__/dhcp-toggle.test.tsx index 1dfaba18d..fb1e20ed0 100644 --- a/client_v2/src/__tests__/dhcp-toggle.test.tsx +++ b/client_v2/src/__tests__/dhcp-toggle.test.tsx @@ -9,6 +9,8 @@ const mocks = vi.hoisted(() => ({ interface_name: '', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }, })); @@ -28,18 +30,51 @@ describe('DhcpToggle', () => { interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const { container } = render(() => 'eth0'} />); const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; expect(input.checked).toBe(true); }); - it('calls toggleDhcp with enabled:false + interface_name when toggled ON + calls onToggleOn', () => { + it('when toggled ON without v4 config, reverts UI and calls onToggleOn without toggleDhcp', () => { mocks.dhcpState = { enabled: false, interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, + }; + const onToggleOn = vi.fn(); + const { container } = render(() => ( + 'eth0'} onToggleOn={onToggleOn} /> + )); + const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; + fireEvent.change(input, { target: { checked: true } }); + // Shadow signal reverts — the switch should appear unchecked. + expect(input.checked).toBe(false); + // Backend must NOT be called — config is not yet filled. + expect(mocks.toggleDhcp).not.toHaveBeenCalled(); + // Config modal should open so the user can fill v4 settings. + expect(onToggleOn).toHaveBeenCalledOnce(); + }); + + it('when toggled ON with existing v4 config, calls toggleDhcp without onToggleOn', () => { + mocks.dhcpState = { + enabled: false, + interface_name: 'eth0', + processingDhcp: false, + processingConfig: false, + v4: { + gateway_ip: '192.168.1.1', + subnet_mask: '255.255.255.0', + range_start: '192.168.1.50', + range_end: '192.168.1.100', + lease_duration: 86400, + }, + v6: { range_start: '', lease_duration: 0 }, }; const onToggleOn = vi.fn(); const { container } = render(() => ( @@ -51,9 +86,11 @@ describe('DhcpToggle', () => { expect.objectContaining({ enabled: false, interface_name: 'eth0', + v4: expect.objectContaining({ gateway_ip: '192.168.1.1' }), }), ); - expect(onToggleOn).toHaveBeenCalledOnce(); + // v4 is configured — no need to open the config modal. + expect(onToggleOn).not.toHaveBeenCalled(); }); it('calls toggleDhcp with enabled:true when toggled OFF, does NOT call onToggleOn', () => { @@ -62,6 +99,8 @@ describe('DhcpToggle', () => { interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const onToggleOn = vi.fn(); const { container } = render(() => ( @@ -79,6 +118,8 @@ describe('DhcpToggle', () => { interface_name: '', processingConfig: true, processingDhcp: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const { container } = render(() => ''} />); const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; diff --git a/client_v2/src/__tests__/dns-config-store.test.ts b/client_v2/src/__tests__/dns-config-store.test.ts index c91246aa8..a60868888 100644 --- a/client_v2/src/__tests__/dns-config-store.test.ts +++ b/client_v2/src/__tests__/dns-config-store.test.ts @@ -1,17 +1,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - getDnsConfig: vi.fn(), - setDnsConfig: vi.fn(), + dnsInfo: vi.fn(), + dnsConfig: vi.fn(), addErrorToast: vi.fn(), addSuccessToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - getDnsConfig: mocks.getDnsConfig, - setDnsConfig: mocks.setDnsConfig, - }, +vi.mock('panel/api/generated', () => ({ + dnsInfo: mocks.dnsInfo, + dnsConfig: mocks.dnsConfig, })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast, @@ -27,7 +25,7 @@ describe('getDnsConfig', () => { beforeEach(() => vi.clearAllMocks()); it('defaults null blocking IPs and empty upstream_mode', async () => { - mocks.getDnsConfig.mockResolvedValue({ + mocks.dnsInfo.mockResolvedValue({ blocking_ipv4: null, blocking_ipv6: null, upstream_mode: '', @@ -43,7 +41,7 @@ describe('toggleResolveClients', () => { beforeEach(() => vi.clearAllMocks()); it('toggles resolve_clients and persists', async () => { - mocks.setDnsConfig.mockResolvedValue({}); + mocks.dnsConfig.mockResolvedValue({}); const before = dnsConfigState.resolve_clients; await toggleResolveClients(); @@ -54,12 +52,12 @@ describe('toggleResolveClients', () => { expect(dnsConfigState.resolve_clients).toBe(before); }); - it('calls apiClient.setDnsConfig with inverted value', async () => { - mocks.setDnsConfig.mockResolvedValue({}); + it('calls dnsConfig with inverted value', async () => { + mocks.dnsConfig.mockResolvedValue({}); const before = dnsConfigState.resolve_clients; await toggleResolveClients(); - expect(mocks.setDnsConfig).toHaveBeenCalledWith({ + expect(mocks.dnsConfig).toHaveBeenCalledWith({ resolve_clients: !before, }); }); diff --git a/client_v2/src/__tests__/encryption-store.test.ts b/client_v2/src/__tests__/encryption-store.test.ts index 4f7510757..b97ae953a 100644 --- a/client_v2/src/__tests__/encryption-store.test.ts +++ b/client_v2/src/__tests__/encryption-store.test.ts @@ -1,19 +1,17 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - setTlsConfig: vi.fn(), - validateTlsConfig: vi.fn(), + tlsConfigure: vi.fn(), + tlsValidate: vi.fn(), addErrorToast: vi.fn(), addSuccessToast: vi.fn(), redirectToCurrentProtocol: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - setTlsConfig: mocks.setTlsConfig, - validateTlsConfig: mocks.validateTlsConfig, - getGlobalStatus: vi.fn(), - }, +vi.mock('panel/api/generated', () => ({ + tlsConfigure: mocks.tlsConfigure, + tlsValidate: mocks.tlsValidate, + status: vi.fn(), })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast, @@ -38,7 +36,7 @@ describe('setTlsConfig', () => { beforeEach(() => vi.clearAllMocks()); it('defaults empty ports to 0', async () => { - mocks.setTlsConfig.mockImplementation(async (v: any) => ({ + mocks.tlsConfigure.mockImplementation(async (v: any) => ({ ...v, certificate_chain: '', private_key: '', @@ -46,18 +44,18 @@ describe('setTlsConfig', () => { await setTlsConfig({ certificate_chain: '', private_key: '', - port_https: '', - port_dns_over_tls: '', - port_dns_over_quic: '', + port_https: 0, + port_dns_over_tls: 0, + port_dns_over_quic: 0, }); - const sent = mocks.setTlsConfig.mock.calls[0][0]; + const sent = mocks.tlsConfigure.mock.calls[0][0]; expect(sent.port_https).toBe(0); expect(sent.port_dns_over_tls).toBe(0); expect(sent.port_dns_over_quic).toBe(0); }); it('clears validation status fields when resetValidationStatus is called', async () => { - mocks.validateTlsConfig.mockResolvedValue({ + mocks.tlsValidate.mockResolvedValue({ valid_chain: true, valid_cert: true, valid_key: true, @@ -91,7 +89,7 @@ describe('setTlsConfig', () => { expect(encryptionState.warning_validation).toBe(''); expect(encryptionState.subject).toBe(''); expect(encryptionState.issuer).toBe(''); - expect(encryptionState.key_type).toBe(''); + expect(encryptionState.key_type).toBeUndefined(); expect(encryptionState.dns_names).toBeNull(); }); @@ -100,7 +98,7 @@ describe('setTlsConfig', () => { value: { protocol: 'http:' }, writable: true, }); - mocks.setTlsConfig.mockImplementation(async (v: any) => ({ + mocks.tlsConfigure.mockImplementation(async (v: any) => ({ ...v, certificate_chain: '', private_key: '', @@ -127,7 +125,7 @@ describe('setTlsConfig', () => { value: { protocol: 'https:' }, writable: true, }); - mocks.setTlsConfig.mockImplementation(async (v: any) => ({ + mocks.tlsConfigure.mockImplementation(async (v: any) => ({ ...v, certificate_chain: '', private_key: '', @@ -148,7 +146,7 @@ describe('setTlsConfig', () => { value: { protocol: 'https:' }, writable: true, }); - mocks.setTlsConfig.mockImplementation(async (v: any) => ({ + mocks.tlsConfigure.mockImplementation(async (v: any) => ({ ...v, certificate_chain: '', private_key: '', diff --git a/client_v2/src/__tests__/filtering-store.test.ts b/client_v2/src/__tests__/filtering-store.test.ts index 6a1375f61..15ffc77df 100644 --- a/client_v2/src/__tests__/filtering-store.test.ts +++ b/client_v2/src/__tests__/filtering-store.test.ts @@ -15,12 +15,10 @@ const mocks = vi.hoisted(() => ({ addErrorToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - setRules: mocks.apiSetRules, - addFilter: mocks.apiAddFilter, - getFilteringStatus: mocks.apiGetFilteringStatus, - }, +vi.mock('panel/api/generated', () => ({ + filteringSetRules: mocks.apiSetRules, + filteringAddURL: mocks.apiAddFilter, + filteringStatus: mocks.apiGetFilteringStatus, })); vi.mock('panel/stores/toasts', () => ({ diff --git a/client_v2/src/__tests__/filteringStore.test.ts b/client_v2/src/__tests__/filteringStore.test.ts index 6a1375f61..15ffc77df 100644 --- a/client_v2/src/__tests__/filteringStore.test.ts +++ b/client_v2/src/__tests__/filteringStore.test.ts @@ -15,12 +15,10 @@ const mocks = vi.hoisted(() => ({ addErrorToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - setRules: mocks.apiSetRules, - addFilter: mocks.apiAddFilter, - getFilteringStatus: mocks.apiGetFilteringStatus, - }, +vi.mock('panel/api/generated', () => ({ + filteringSetRules: mocks.apiSetRules, + filteringAddURL: mocks.apiAddFilter, + filteringStatus: mocks.apiGetFilteringStatus, })); vi.mock('panel/stores/toasts', () => ({ diff --git a/client_v2/src/__tests__/helpers/get-browser-language.test.ts b/client_v2/src/__tests__/helpers/get-browser-language.test.ts new file mode 100644 index 000000000..e208164c3 --- /dev/null +++ b/client_v2/src/__tests__/helpers/get-browser-language.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it, vi, afterEach } from 'vitest'; + +import { getBrowserLanguage } from '../../helpers/helpers'; + +// Mock the twosky LANGUAGES map to keep tests deterministic +vi.mock('../../helpers/twosky', () => ({ + LANGUAGES: { + en: 'English', + de: 'Deutsch', + fr: 'Français', + 'zh-cn': '简体中文', + 'pt-br': 'Português (BR)', + }, + LANGUAGE_NAMES: { + en: 'English', + de: 'Deutsch', + fr: 'Français', + 'zh-cn': '简体中文', + 'pt-br': 'Português (BR)', + }, + BASE_LOCALE: 'en', +})); + +const { LocalStorageHelper } = await import('../../helpers/localStorageHelper'); +const getStoredItemSpy = vi.spyOn(LocalStorageHelper, 'getItem'); + +const navigatorSpy = vi.spyOn(globalThis.navigator, 'language', 'get'); + +afterEach(() => { + getStoredItemSpy.mockReset(); + navigatorSpy.mockReset(); +}); + +describe('getBrowserLanguage', () => { + describe('localStorage takes priority', () => { + it('returns stored language when it matches a supported locale', () => { + getStoredItemSpy.mockReturnValue('de'); + expect(getBrowserLanguage()).toBe('de'); + }); + + it('falls past localStorage when the stored code is unsupported', () => { + getStoredItemSpy.mockReturnValue('xx'); + navigatorSpy.mockReturnValue('fr'); + expect(getBrowserLanguage()).toBe('fr'); + }); + }); + + describe('browser language detection', () => { + it('matches full locale code like zh-cn', () => { + navigatorSpy.mockReturnValue('zh-CN'); + expect(getBrowserLanguage()).toBe('zh-cn'); + }); + + it('matches base language when full locale is unsupported', () => { + navigatorSpy.mockReturnValue('fr-FR'); + expect(getBrowserLanguage()).toBe('fr'); + }); + + it('matches base language from pt-BR', () => { + navigatorSpy.mockReturnValue('pt-BR'); + // "pt-br" is a supported full locale, so it matches directly + expect(getBrowserLanguage()).toBe('pt-br'); + }); + + it('falls back to base language for unsupported region', () => { + navigatorSpy.mockReturnValue('de-AT'); + // "de-at" is NOT in LANGUAGES, but base "de" is + expect(getBrowserLanguage()).toBe('de'); + }); + }); + + describe('fallback to en', () => { + it('returns en when localStorage is empty and navigator is missing', () => { + getStoredItemSpy.mockReturnValue(null); + navigatorSpy.mockReturnValue(''); + expect(getBrowserLanguage()).toBe('en'); + }); + + it('returns en when both sources yield unsupported codes', () => { + getStoredItemSpy.mockReturnValue('yy'); + navigatorSpy.mockReturnValue('zz-ZZ'); + expect(getBrowserLanguage()).toBe('en'); + }); + }); +}); diff --git a/client_v2/src/__tests__/helpers/helpers-scalar.spec.ts b/client_v2/src/__tests__/helpers/helpers-scalar.spec.ts new file mode 100644 index 000000000..d1b9424c6 --- /dev/null +++ b/client_v2/src/__tests__/helpers/helpers-scalar.spec.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; + +import { + msToSeconds, + msToMinutes, + msToHours, + secondsToMilliseconds, + splitByNewLine, + trimLinesAndRemoveEmpty, + normalizeRulesTextarea, + captitalizeWords, + getWebAddress, + getInterfaceIp, + isIpInCidr, + parseSubnetMask, + subnetMaskToBitMask, +} from '../../helpers/helpers'; + +describe('ms helpers', () => { + it('converts ms -> seconds', () => { + expect(msToSeconds(1500)).toBe(1); + }); + it('converts ms -> minutes', () => { + expect(msToMinutes(120_000)).toBe(2); + }); + it('converts ms -> hours', () => { + expect(msToHours(3_600_000)).toBe(1); + }); +}); + +describe('secondsToMilliseconds', () => { + it('multiplies by 1000', () => { + expect(secondsToMilliseconds(3)).toBe(3000); + }); + it('returns falsy input as-is', () => { + expect(secondsToMilliseconds(0)).toBe(0); + // The current implementation returns `seconds` unchanged if falsy + expect(secondsToMilliseconds(undefined as unknown as number)).toBe(undefined); + }); +}); + +describe('splitByNewLine', () => { + it('splits and removes empty lines', () => { + expect(splitByNewLine('a\nb\n\nc')).toStrictEqual(['a', 'b', 'c']); + }); + it('returns [] for falsy input', () => { + expect(splitByNewLine('')).toStrictEqual([]); + expect(splitByNewLine(undefined as unknown as string)).toStrictEqual([]); + }); +}); + +describe('trimLinesAndRemoveEmpty', () => { + it('trims lines', () => { + expect(trimLinesAndRemoveEmpty(' a \n\n b ')).toBe('a\nb'); + }); +}); + +describe('normalizeRulesTextarea', () => { + it('strips leading newlines and collapses repeated blank lines', () => { + expect(normalizeRulesTextarea('\na\n\nb')).toBe('a\nb'); + }); +}); + +describe('captitalizeWords', () => { + it('capitalizes each word splitting by space, dash, or underscore', () => { + expect(captitalizeWords('safe_browsing mode-test')).toBe('Safe Browsing Mode Test'); + }); +}); + +describe('getWebAddress', () => { + it('builds http url omitting standard port 80', () => { + expect(getWebAddress('192.168.1.1', 80)).toBe('http://192.168.1.1'); + }); + it('appends non-standard port', () => { + expect(getWebAddress('192.168.1.1', 8080)).toBe('http://192.168.1.1:8080'); + }); + it('brackets IPv6 with zone encoding', () => { + expect(getWebAddress('fe80::1%eth0', 80)).toBe('http://[fe80::1%25eth0]'); + }); +}); + +describe('getInterfaceIp', () => { + it('prefers IPv4 over IPv6', () => { + expect(getInterfaceIp({ ip_addresses: ['10.0.0.1', 'fe80::1'] })).toBe('10.0.0.1'); + }); + it('skips IPv6 link-local when IPv4 present', () => { + expect(getInterfaceIp({ ip_addresses: ['192.168.1.1', 'fe80::1'] })).toBe('192.168.1.1'); + }); + it('falls back to IPv6 global without zone', () => { + expect(getInterfaceIp({ ip_addresses: ['2001:db8::1'] })).toBe('2001:db8::1'); + }); + it('returns undefined when no addresses', () => { + expect(getInterfaceIp({ ip_addresses: [] })).toBeUndefined(); + }); +}); + +describe('isIpInCidr', () => { + it('matches IP inside CIDR', () => { + expect(isIpInCidr('192.168.1.5', '192.168.1.0/24')).toBe(true); + }); + it('rejects IP outside CIDR', () => { + expect(isIpInCidr('10.0.0.1', '192.168.1.0/24')).toBe(false); + }); +}); + +describe('parseSubnetMask', () => { + it('returns prefix length for valid mask', () => { + expect(parseSubnetMask('255.255.255.0')).toBe(24); + }); + it('returns null for invalid mask string', () => { + expect(parseSubnetMask('not-a-mask')).toBeNull(); + }); +}); + +describe('subnetMaskToBitMask', () => { + it('computes prefix length from dotted mask', () => { + expect(subnetMaskToBitMask('255.255.255.0')).toBe(24); + }); +}); diff --git a/client_v2/src/__tests__/helpers/helpers-stats.spec.ts b/client_v2/src/__tests__/helpers/helpers-stats.spec.ts new file mode 100644 index 000000000..2f3eec624 --- /dev/null +++ b/client_v2/src/__tests__/helpers/helpers-stats.spec.ts @@ -0,0 +1,199 @@ +import { describe, expect, it } from 'vitest'; + +import { + normalizeTopStats, + addClientInfo, + normalizeTopClients, + normalizeFilters, + normalizeFilteringStatus, + getParamsForClientsSearch, + checkFiltered, + checkBlockedService, + getPathWithQueryString, + getSpecialFilterName, + getServiceName, + normalizeWhois, + normalizeLogs, +} from '../../helpers/helpers'; +import type { ClientsFindEntry } from '../../api/model/clientsFindEntry'; +import type { FilteringReason } from '../../api/model/filteringReason'; + +describe('normalizeTopStats', () => { + it('converts {name -> count} objects to {name, count} array', () => { + expect(normalizeTopStats([{ '192.168.1.1': 42 }, { 'example.com': 5 }])).toStrictEqual([ + { name: '192.168.1.1', count: 42 }, + { name: 'example.com', count: 5 }, + ]); + }); +}); + +describe('addClientInfo', () => { + it('resolves client info by param key', () => { + const data = [{ name: '192.168.1.1', count: 1 }]; + const clients: ClientsFindEntry[] = [{ '192.168.1.1': { name: 'MyPhone' } }]; + expect(addClientInfo(data, clients, 'name')).toStrictEqual([ + { name: '192.168.1.1', count: 1, info: { name: 'MyPhone' } }, + ]); + }); +}); + +describe('normalizeTopClients', () => { + it('splits into auto/configured by name and info', () => { + const r = normalizeTopClients([ + { name: '192.168.1.1', count: 7, info: { name: 'MyPhone' } }, + ]); + expect(r.auto).toStrictEqual({ '192.168.1.1': 7 }); + expect(r.configured).toStrictEqual({ MyPhone: 7 }); + }); +}); + +describe('normalizeFilters', () => { + it('maps snake_case API fields to camelCase with defaults', () => { + expect( + normalizeFilters([ + { + id: 1, + url: 'http://example.com/list.txt', + enabled: true, + last_updated: '2024-01-01', + name: 'My List', + rules_count: 100, + }, + ]), + ).toStrictEqual([ + { + id: 1, + url: 'http://example.com/list.txt', + enabled: true, + lastUpdated: '2024-01-01', + name: 'My List', + rulesCount: 100, + }, + ]); + }); + it('returns [] for falsy input', () => { + expect(normalizeFilters(undefined)).toStrictEqual([]); + }); +}); + +describe('normalizeFilteringStatus', () => { + it('normalizes full status with user_rules', () => { + const r = normalizeFilteringStatus({ + enabled: true, + filters: [], + whitelist_filters: [], + user_rules: ['rule1', 'rule2'], + interval: 24, + }); + expect(r.enabled).toBe(true); + expect(r.interval).toBe(24); + expect(r.userRules).toBe('rule1\nrule2'); + }); +}); + +describe('getParamsForClientsSearch', () => { + it('collects unique client ids from TopStat[]', () => { + expect( + getParamsForClientsSearch( + [ + { name: 'client-a', count: 1 }, + { name: 'client-b', count: 2 }, + ], + 'name', + ), + ).toStrictEqual({ clients: [{ id: 'client-a' }, { id: 'client-b' }] }); + }); + it('includes additional param when provided', () => { + const r = getParamsForClientsSearch([{ name: 'a', count: 1 }], 'name', 'count'); + expect(r.clients).toStrictEqual([{ id: 'a' }, { id: 1 }]); + }); +}); + +describe('checkFiltered / checkBlockedService', () => { + it('checkFiltered returns true for Filtered* reasons', () => { + expect(checkFiltered('FilteredBlackList' as FilteringReason)).toBe(true); + }); + it('checkFiltered returns false for NotFiltered* reasons', () => { + expect(checkFiltered('NotFilteredNotFound' as FilteringReason)).toBe(false); + }); + it('checkBlockedService returns true for FilteredBlockedService', () => { + expect(checkBlockedService('FilteredBlockedService' as FilteringReason)).toBe(true); + }); +}); + +describe('getPathWithQueryString', () => { + it('serializes params, skips empty/undefined values, repeats arrays', () => { + const r = getPathWithQueryString('/endpoint', { + a: '1', + b: '', + c: undefined, + d: ['x', 'y'], + }); + expect(r).toBe('/endpoint?a=1&d=x&d=y'); + }); + it('handles null params gracefully', () => { + const r = getPathWithQueryString('/p', { a: '1', b: null }); + expect(r).toBe('/p?a=1'); + }); + it('handles undefined params arg', () => { + expect(getPathWithQueryString('/p', undefined)).toBe('/p?'); + }); +}); + +describe('getSpecialFilterName', () => { + it('returns localized name for known special filter IDs', () => { + expect(typeof getSpecialFilterName(0)).toBe('string'); + expect(typeof getSpecialFilterName(-1)).toBe('string'); + expect(typeof getSpecialFilterName(-5)).toBe('string'); + }); +}); + +describe('getServiceName', () => { + it('returns name for matching service id', () => { + expect(getServiceName([{ id: 'svc1', name: 'My Service' }], 'svc1')).toBe('My Service'); + }); + it('returns undefined for unknown id', () => { + expect(getServiceName([{ id: 'svc1', name: 'My Service' }], 'svc-unknown')).toBeUndefined(); + }); +}); + +describe('normalizeWhois', () => { + it('derives location from city and country', () => { + expect(normalizeWhois({ city: 'NY', country: 'US', orgname: 'Example' })).toMatchObject({ + location: 'US, NY', + orgname: 'Example', + }); + }); + it('uses only country when city absent', () => { + expect(normalizeWhois({ country: 'DE', orgname: 'Org' })).toMatchObject({ + location: 'DE', + }); + }); + it('returns placeholder defaults for empty whois', () => { + expect(normalizeWhois({})).toMatchObject({ + location: 'New York, US', + orgname: 'Example Organization', + }); + }); +}); + +describe('normalizeLogs', () => { + it('maps query log item to normalized shape', () => { + const [item] = normalizeLogs([ + { + time: '2024-01-01T00:00:00Z', + question: { + name: 'example.com', + unicode_name: 'example.com', + type: 'A', + }, + answer: [{ value: '1.2.3.4', type: 'A', ttl: 60 }], + status: 'processed', + }, + ]); + expect(item.domain).toBe('example.com'); + expect(item.unicodeName).toBe('example.com'); + expect(item.type).toBe('A'); + expect(item.response).toStrictEqual([{ value: '1.2.3.4', type: 'A', ttl: 60 }]); + }); +}); diff --git a/client_v2/src/__tests__/helpers/normalize-server-name.test.ts b/client_v2/src/__tests__/helpers/normalize-server-name.test.ts new file mode 100644 index 000000000..409b35d38 --- /dev/null +++ b/client_v2/src/__tests__/helpers/normalize-server-name.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeServerName } from 'panel/helpers/form'; + +describe('normalizeServerName', () => { + it('returns plain hostname unchanged', () => { + expect(normalizeServerName('example.com')).toBe('example.com'); + }); + + it('returns subdomain unchanged', () => { + expect(normalizeServerName('dns.example.com')).toBe('dns.example.com'); + }); + + it('strips trailing dot (FQDN notation)', () => { + expect(normalizeServerName('example.com.')).toBe('example.com'); + }); + + it('trims leading and trailing whitespace', () => { + expect(normalizeServerName(' example.com ')).toBe('example.com'); + }); + + it('strips https:// prefix', () => { + expect(normalizeServerName('https://example.com')).toBe('example.com'); + }); + + it('strips http:// prefix', () => { + expect(normalizeServerName('http://example.com')).toBe('example.com'); + }); + + it('strips trailing slash', () => { + expect(normalizeServerName('https://example.com/')).toBe('example.com'); + }); + + it('strips all three: protocol, trailing slash, and trailing dot', () => { + expect(normalizeServerName(' https://example.com/. ')).toBe('example.com'); + }); + + it('returns empty string for empty input', () => { + expect(normalizeServerName('')).toBe(''); + }); + + it('returns empty string for whitespace-only input', () => { + expect(normalizeServerName(' ')).toBe(''); + }); + + it('returns empty string for a bare dot', () => { + expect(normalizeServerName('.')).toBe(''); + }); + + // ── No silent rewriting beyond protocol and trailing slash ───── + + it('does NOT strip port', () => { + expect(normalizeServerName('example.com:443')).toBe('example.com:443'); + }); + + it('does NOT strip path', () => { + expect(normalizeServerName('example.com/dns-query')).toBe('example.com/dns-query'); + }); + + it('does NOT strip query string', () => { + expect(normalizeServerName('example.com?param=1')).toBe('example.com?param=1'); + }); + + it('does NOT strip fragment', () => { + expect(normalizeServerName('example.com#section')).toBe('example.com#section'); + }); + + it('preserves single-label hostname (localhost)', () => { + expect(normalizeServerName('localhost')).toBe('localhost'); + }); + + it('preserves IDN hostname', () => { + expect(normalizeServerName('münchen.example.com')).toBe('münchen.example.com'); + }); + + it('preserves IPv4 address', () => { + expect(normalizeServerName('192.168.1.1')).toBe('192.168.1.1'); + }); + + it('preserves hostname with hyphens', () => { + expect(normalizeServerName('my-dns-host.example.com')).toBe('my-dns-host.example.com'); + }); + + it('preserves hostname with digits', () => { + expect(normalizeServerName('dns01.example.com')).toBe('dns01.example.com'); + }); + + it('preserves hostname containing "https" text', () => { + expect(normalizeServerName('https-only.example.com')).toBe('https-only.example.com'); + }); +}); diff --git a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts deleted file mode 100644 index e54610866..000000000 --- a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; - -vi.mock('panel/common/intl', () => ({ - default: { - getMessage: vi.fn((key: string, values?: Record) => { - if (key === 'form_error_format_line') { - return `Invalid format on line ${values?.line}`; - } - if (key === 'form_error_format_lines') { - return `Invalid format on lines ${values?.lines}`; - } - if (key === 'form_error_format') { - return 'Invalid format'; - } - return key; - }), - }, -})); - -import { validateDomainsPerLine } from 'panel/helpers/validators'; - -describe('validateDomainsPerLine', () => { - it('returns undefined for empty string', () => { - expect(validateDomainsPerLine('')).toBeUndefined(); - }); - - it('returns undefined for plain domain', () => { - expect(validateDomainsPerLine('example.org')).toBeUndefined(); - }); - - it('returns undefined for wildcard domain', () => { - expect(validateDomainsPerLine('*.example.org')).toBeUndefined(); - }); - - it('returns undefined for AdGuard URL filter rule', () => { - expect(validateDomainsPerLine('||example.org^')).toBeUndefined(); - }); - - it('returns undefined for regex pattern', () => { - expect(validateDomainsPerLine('/regex.pattern/')).toBeUndefined(); - }); - - it('returns undefined for comment line', () => { - expect(validateDomainsPerLine('# this is a comment')).toBeUndefined(); - }); - - it('rejects !-prefixed filter rule that would otherwise pass dot check', () => { - expect(validateDomainsPerLine('! ||example.org^')).toBeTruthy(); - }); - - it('rejects only ! lines as invalid', () => { - expect(validateDomainsPerLine('! first\n! second')).toBeTruthy(); - }); - - it('returns undefined for mixed valid lines with comments', () => { - expect( - validateDomainsPerLine('# comment\nexample.org\n||ads.example.org^'), - ).toBeUndefined(); - }); - - it('returns "Invalid format" for entry without dot', () => { - expect(validateDomainsPerLine('notadomain')).toBe('Invalid format'); - }); - - it('returns "Invalid format on line N" when specific line has no dot', () => { - expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2'); - }); - - it('returns "Invalid format on lines N, M" when multiple lines invalid', () => { - expect(validateDomainsPerLine('nodot1\nexample.org\nnodot2')).toBe( - 'Invalid format on lines 1, 3', - ); - }); - - it('returns "Invalid format" for single invalid line with trailing newline', () => { - expect(validateDomainsPerLine('notadomain\n')).toBe('Invalid format'); - }); - - it('returns "Invalid format" for single invalid line with leading newline', () => { - expect(validateDomainsPerLine('\nnotadomain')).toBe('Invalid format'); - }); - - it('returns "Invalid format on lines 1, 2" when both lines invalid', () => { - expect(validateDomainsPerLine('nodot1\nnodot2')).toBe('Invalid format on lines 1, 2'); - }); - - it('returns "Invalid format on line 2" when second line invalid in multi-content input', () => { - expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2'); - }); - - it('returns undefined for all-blank input', () => { - expect(validateDomainsPerLine('\n\n')).toBeUndefined(); - }); - - it('handles blank line between two invalid lines', () => { - expect(validateDomainsPerLine('nodot1\n\nnodot2')).toBe('Invalid format on lines 1, 3'); - }); - - it('returns "Invalid format" for comment-then-invalid (one content line)', () => { - expect(validateDomainsPerLine('# comment\nnotadomain')).toBe('Invalid format'); - }); - - it('returns "Invalid format" for invalid-then-comment (one content line)', () => { - expect(validateDomainsPerLine('notadomain\n# comment')).toBe('Invalid format'); - }); -}); diff --git a/client_v2/src/__tests__/install-store.test.ts b/client_v2/src/__tests__/install-store.test.ts index b4f33f1dc..169669d2f 100644 --- a/client_v2/src/__tests__/install-store.test.ts +++ b/client_v2/src/__tests__/install-store.test.ts @@ -2,16 +2,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the API client. const mockCheckConfig = vi.fn(); -vi.mock('panel/api/Api', () => ({ - apiClient: { - checkConfig: (...args: unknown[]) => mockCheckConfig(...args), - getDefaultAddresses: vi.fn(), - setAllSettings: vi.fn(), - }, +vi.mock('panel/api/generated', () => ({ + installCheckConfig: (...args: unknown[]) => mockCheckConfig(...args), + installGetAddresses: vi.fn(), + installConfigure: vi.fn(), })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: vi.fn() })); -vi.mock('panel/helpers/constants', () => ({ +vi.mock('panel/helpers/constants', async (importOriginal) => ({ + ...(await importOriginal()), ALL_INTERFACES_IP: '0.0.0.0', INSTALL_FIRST_STEP: 1, STANDARD_DNS_PORT: 53, diff --git a/client_v2/src/__tests__/intl.test.ts b/client_v2/src/__tests__/intl.test.ts index a9a40d94a..dbcdd1f9a 100644 --- a/client_v2/src/__tests__/intl.test.ts +++ b/client_v2/src/__tests__/intl.test.ts @@ -1,6 +1,18 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { translate } from '@adguard/translate'; -import { createSolidDefaultValues, solidMessageConstructor } from '../common/intl/index'; +import { LocalStorageHelper } from '../helpers/localStorageHelper'; + +// Ensure the real constants module is available even when other test files +// (e.g., install-store.test.ts) mock it with a partial stub. +vi.mock('panel/helpers/constants', async (importOriginal) => + importOriginal(), +); + +import { + createSolidDefaultValues, + solidMessageConstructor, + getInitialLanguage, +} from '../common/intl/index'; /** Helper to create real DOM elements for test assertions (SolidJS has no h() export) */ const h = (tag: string, props: Record | null, children?: string) => { @@ -205,3 +217,92 @@ describe('intl.getMessage — missing placeholder fallback', () => { expect(result).toBe('plural_with_value'); }); }); + +describe('getInitialLanguage', () => { + const LANGUAGE_KEY = 'language'; + + const setLocation = (href: string) => { + delete (globalThis as any).location; + (globalThis as any).location = new URL(href); + }; + + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + // Default: a URL with no lang param (so tests without setLocation work) + setLocation('http://127.0.0.1:3001/login.html'); + }); + + afterEach(() => { + localStorage.clear(); + delete (globalThis as any).location; + }); + + const storedLang = () => LocalStorageHelper.getItem(LANGUAGE_KEY); + + it('resolves a valid URL query param and persists to localStorage', () => { + setLocation('http://127.0.0.1:3001/login.html?lang=zh-cn'); + + const result = getInitialLanguage(); + + expect(result).toBe('zh-cn'); + expect(storedLang()).toBe('zh-cn'); + }); + + it('resolves an invalid URL query param to en and persists en', () => { + setLocation('http://127.0.0.1:3001/login.html?lang=garbage'); + + const result = getInitialLanguage(); + + expect(result).toBe('en'); + expect(storedLang()).toBe('en'); + }); + + it('resolves abbreviated zh to zh-cn', () => { + setLocation('http://127.0.0.1:3001/login.html?lang=zh'); + + const result = getInitialLanguage(); + + expect(result).toBe('zh-cn'); + expect(storedLang()).toBe('zh-cn'); + }); + + it('falls back to localStorage when no URL param is present', () => { + LocalStorageHelper.setItem(LANGUAGE_KEY, 'de'); + + const result = getInitialLanguage(); + + expect(result).toBe('de'); + }); + + it('falls back to navigator.language when no URL param or localStorage', () => { + vi.stubGlobal('navigator', { language: 'fr-FR' }); + + const result = getInitialLanguage(); + + expect(result).toBe('fr'); + vi.unstubAllGlobals(); + }); + + it('falls back to en when no source provides a valid language', () => { + // No URL param, no localStorage, stub navigator to a nonsense value + vi.stubGlobal('navigator', { language: 'xx-XX' }); + + const result = getInitialLanguage(); + + expect(result).toBe('en'); + vi.unstubAllGlobals(); + }); + + it('returns en when typeof window is undefined (SSR)', () => { + const originalWindow = globalThis.window; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (globalThis as any).window; + + const result = getInitialLanguage(); + + expect(result).toBe('en'); + + (globalThis as any).window = originalWindow; + }); +}); diff --git a/client_v2/src/__tests__/login-store.test.ts b/client_v2/src/__tests__/login-store.test.ts index 66eb27235..d025555a0 100644 --- a/client_v2/src/__tests__/login-store.test.ts +++ b/client_v2/src/__tests__/login-store.test.ts @@ -2,10 +2,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the API client before importing the store. const mockLogin = vi.fn().mockResolvedValue(undefined); -vi.mock('panel/api/Api', () => ({ - apiClient: { +vi.mock('panel/api/generated', () => ({ login: (...args: unknown[]) => mockLogin(...args), - }, })); // Mock addErrorToast so it does not depend on other stores. @@ -13,8 +11,10 @@ vi.mock('panel/stores/toasts', () => ({ addErrorToast: vi.fn(), })); -// Mock HTML_PAGES constant. -vi.mock('panel/helpers/constants', () => ({ +// Mock HTML_PAGES constant, but pass through all real exports so +// transitive dependencies (e.g., LANGUAGE_QUERY_PARAM in intl) work. +vi.mock('panel/helpers/constants', async (importOriginal) => ({ + ...(await importOriginal()), HTML_PAGES: { LOGIN: '/login.html', MAIN: '/dashboard.html' }, })); diff --git a/client_v2/src/__tests__/queryLogStore.test.ts b/client_v2/src/__tests__/queryLogStore.test.ts index 82551e457..902936ba4 100644 --- a/client_v2/src/__tests__/queryLogStore.test.ts +++ b/client_v2/src/__tests__/queryLogStore.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { getAdditionalLogs, setFilteredLogs, queryLogsState } from 'panel/stores/queryLogs'; -import { apiClient } from 'panel/api/Api'; +import { queryLog } from 'panel/api/generated'; -vi.mock('panel/api/Api', () => ({ - apiClient: { - getQueryLog: vi.fn(), - }, +vi.mock('panel/api/generated', () => ({ + queryLog: vi.fn(), })); vi.mock('panel/stores/toasts', () => ({ @@ -19,14 +17,14 @@ describe('queryLogs store', () => { }); it('getAdditionalLogs appends filtered rows using the oldest cursor and filter', async () => { - (apiClient.getQueryLog as any).mockReset(); + (queryLog as any).mockReset(); // Seed state: a full first page (20 rewritten) with more behind it. const fullPage = Array.from({ length: 20 }, () => ({ reason: 'Rewrite', question: {}, })); - (apiClient.getQueryLog as any).mockResolvedValueOnce({ + (queryLog as any).mockResolvedValueOnce({ data: fullPage, oldest: 'cur', }); @@ -35,7 +33,7 @@ describe('queryLogs store', () => { expect(queryLogsState.logs).toHaveLength(20); // Load more: one extra rewritten entry, then end of log. - (apiClient.getQueryLog as any).mockResolvedValueOnce({ + (queryLog as any).mockResolvedValueOnce({ data: [{ reason: 'Rewrite', question: {} }], oldest: '', }); @@ -43,7 +41,7 @@ describe('queryLogs store', () => { await getAdditionalLogs(); // The load-more request must carry the cursor and the reason filter. - expect(apiClient.getQueryLog).toHaveBeenLastCalledWith( + expect(queryLog).toHaveBeenLastCalledWith( expect.objectContaining({ older_than: 'cur', reason: expect.arrayContaining([ @@ -55,7 +53,7 @@ describe('queryLogs store', () => { }), ); // Must NOT send the deprecated response_status - const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0]; + const lastCall = (queryLog as any).mock.calls.at(-1)[0]; expect(lastCall).not.toHaveProperty('response_status'); expect(queryLogsState.logs).toHaveLength(21); @@ -64,16 +62,16 @@ describe('queryLogs store', () => { }); it('setFilteredLogs sends reason strings for blocked status', async () => { - (apiClient.getQueryLog as any).mockReset(); - (apiClient.getQueryLog as any).mockResolvedValue({ + (queryLog as any).mockReset(); + (queryLog as any).mockResolvedValue({ data: [{ reason: 'FilteredBlackList', question: {} }], oldest: '', }); await setFilteredLogs({ search: '', status: 'blocked', reason: 'all' }); - const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0]; - expect(apiClient.getQueryLog).toHaveBeenLastCalledWith( + const lastCall = (queryLog as any).mock.calls.at(-1)[0]; + expect(queryLog).toHaveBeenLastCalledWith( expect.objectContaining({ reason: expect.arrayContaining([ 'FilteredBlackList', @@ -87,7 +85,7 @@ describe('queryLogs store', () => { }); it('does not mark the log as complete when additional loading stops', async () => { - (apiClient.getQueryLog as any).mockResolvedValue({ + (queryLog as any).mockResolvedValue({ data: [], oldest: 'next-cursor', }); @@ -99,7 +97,7 @@ describe('queryLogs store', () => { }); it('accumulates pages until oldest is empty (short-polling)', async () => { - (apiClient.getQueryLog as any) + (queryLog as any) .mockResolvedValueOnce({ data: [{ reason: 'Rewrite' }], oldest: 'cur', @@ -112,7 +110,37 @@ describe('queryLogs store', () => { }); await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' }); - expect(apiClient.getQueryLog).toHaveBeenCalledTimes(2); + expect(queryLog).toHaveBeenCalledTimes(2); expect(queryLogsState.processingGetLogs).toBe(false); }); + + it('always sends limit=20 to prevent loading all records at once', async () => { + (queryLog as any).mockReset(); + (queryLog as any) + .mockResolvedValueOnce({ + data: Array.from({ length: 20 }, () => ({ reason: 'Rewrite', question: {} })), + oldest: 'cursor1', + }) + .mockResolvedValueOnce({ + data: [{ reason: 'Rewrite', question: {} }], + oldest: '', + }); + + await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' }); + + for (const call of (queryLog as any).mock.calls) { + expect(call[0]).toHaveProperty('limit', 20); + } + expect(queryLog).not.toHaveBeenCalledWith(expect.not.objectContaining({ limit: 20 })); + + (queryLog as any).mockReset(); + (queryLog as any).mockResolvedValueOnce({ + data: [{ reason: 'Rewrite', question: {} }], + oldest: '', + }); + + await getAdditionalLogs(); + + expect(queryLog).toHaveBeenCalledWith(expect.objectContaining({ limit: 20 })); + }); }); diff --git a/client_v2/src/__tests__/stats-store.test.ts b/client_v2/src/__tests__/stats-store.test.ts index 74559566c..f0f198859 100644 --- a/client_v2/src/__tests__/stats-store.test.ts +++ b/client_v2/src/__tests__/stats-store.test.ts @@ -1,16 +1,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mocks = vi.hoisted(() => ({ - getStats: vi.fn(), - searchClients: vi.fn(), + stats: vi.fn(), + clientsSearch: vi.fn(), addErrorToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - getStats: mocks.getStats, - searchClients: mocks.searchClients, - }, +vi.mock('panel/api/generated', () => ({ + stats: mocks.stats, + clientsSearch: mocks.clientsSearch, })); vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast, @@ -21,11 +19,11 @@ import { getStats, statsState } from 'panel/stores/stats'; describe('getStats', () => { beforeEach(() => { vi.clearAllMocks(); - mocks.searchClients.mockResolvedValue([]); + mocks.clientsSearch.mockResolvedValue([]); }); it('enriches top clients and stores normalizedTopClients', async () => { - mocks.getStats.mockResolvedValue({ + mocks.stats.mockResolvedValue({ top_clients: [{ '1.2.3.4': 5 }], avg_processing_time: 0.012, top_blocked_domains: [], @@ -37,7 +35,7 @@ describe('getStats', () => { await getStats(); // searchClients was called with discovered client ids - expect(mocks.searchClients).toHaveBeenCalledWith({ + expect(mocks.clientsSearch).toHaveBeenCalledWith({ clients: [{ id: '1.2.3.4' }], }); // normalizedTopClients is populated (configured bucket carries info name) @@ -45,17 +43,17 @@ describe('getStats', () => { }); it('converts avg_processing_time to milliseconds, falsy passthrough', async () => { - mocks.getStats.mockResolvedValue({ avg_processing_time: 0.012 }); + mocks.stats.mockResolvedValue({ avg_processing_time: 0.012 }); await getStats(); expect(statsState.avgProcessingTime).toBe(12); - mocks.getStats.mockResolvedValue({ avg_processing_time: 0 }); + mocks.stats.mockResolvedValue({ avg_processing_time: 0 }); await getStats(); expect(statsState.avgProcessingTime).toBe(0); // not NaN }); it('converts top_upstreams_avg_time entries from seconds to milliseconds', async () => { - mocks.getStats.mockResolvedValue({ + mocks.stats.mockResolvedValue({ top_upstreams_avg_time: [{ '1.1.1.1': 0.012 }, { '9.9.9.9': 0.3 }], top_clients: [], avg_processing_time: 0, diff --git a/client_v2/src/__tests__/toast.test.tsx b/client_v2/src/__tests__/toast.test.tsx index f6e4d342c..02f3e2283 100644 --- a/client_v2/src/__tests__/toast.test.tsx +++ b/client_v2/src/__tests__/toast.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { render } from '@solidjs/testing-library'; -import Toast from '../components/Toasts/Toast'; +import { Toast } from '../components/Toasts/Toast'; describe('Toast', () => { it('renders the plain message when no options', () => { diff --git a/client_v2/src/__tests__/toggle-blocking.test.ts b/client_v2/src/__tests__/toggle-blocking.test.ts index 53ec146ef..e1d324e79 100644 --- a/client_v2/src/__tests__/toggle-blocking.test.ts +++ b/client_v2/src/__tests__/toggle-blocking.test.ts @@ -22,11 +22,9 @@ const mocks = vi.hoisted(() => ({ addErrorToast: vi.fn(), })); -vi.mock('panel/api/Api', () => ({ - apiClient: { - setRules: mocks.apiSetRules, - getFilteringStatus: mocks.apiGetFilteringStatus, - }, +vi.mock('panel/api/generated', () => ({ + filteringSetRules: mocks.apiSetRules, + filteringStatus: mocks.apiGetFilteringStatus, })); vi.mock('panel/stores/toasts', () => ({ diff --git a/client_v2/src/api/Api.ts b/client_v2/src/api/Api.ts deleted file mode 100644 index 228229d46..000000000 --- a/client_v2/src/api/Api.ts +++ /dev/null @@ -1,746 +0,0 @@ -import type { LocalesType } from 'panel/common/intl'; - -const BASE_URL = 'control'; - -import { getPathWithQueryString } from '../helpers/helpers'; -import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../helpers/constants'; -import intl from '../common/intl'; - -type Theme = (typeof THEMES)[keyof typeof THEMES]; - -class Api { - baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - async makeRequest(path: any, method = 'POST', config: any = {}) { - const pathWithBase = `${this.baseUrl}/${path}`; - const url = config.params - ? getPathWithQueryString(pathWithBase, config.params) - : pathWithBase; - - const fetchConfig: RequestInit = { method }; - const headers: Record = {}; - - if (method !== 'GET' && config.data) { - headers['Content-Type'] = config.headers?.['Content-Type'] || 'application/json'; - fetchConfig.body = - typeof config.data === 'string' ? config.data : JSON.stringify(config.data); - } - - fetchConfig.headers = headers; - - try { - const response = await fetch(url, fetchConfig); - const text = await response.text(); - const data = text - ? (() => { - try { - return JSON.parse(text); - } catch { - return text; - } - })() - : ''; - - if (!response.ok) { - const errorPath = url; - const { pathname } = document.location; - const shouldRedirect = - pathname !== HTML_PAGES.LOGIN && pathname !== HTML_PAGES.INSTALL; - - if (response.status === 403 && shouldRedirect) { - const loginPageUrl = window.location.href.replace( - R_PATH_LAST_PART, - HTML_PAGES.LOGIN, - ); - window.location.replace(loginPageUrl); - return false; - } - - throw new Error(`${errorPath} | ${data} | ${response.status}`); - } - - return data; - } catch (error) { - const errorPath = url; - - if (error instanceof Error && error.message.includes('|')) { - throw error; - } - - throw new Error(`${errorPath} | ${error.message || error}`); - } - } - - // Global methods - GLOBAL_STATUS = { path: 'status', method: 'GET' }; - - GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; - - GLOBAL_VERSION = { path: 'version.json', method: 'POST' }; - - GLOBAL_UPDATE = { path: 'update', method: 'POST' }; - - getGlobalStatus() { - const { path, method } = this.GLOBAL_STATUS; - - return this.makeRequest(path, method); - } - - testUpstream(servers: any) { - const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS; - const config = { - data: servers, - }; - return this.makeRequest(path, method, config); - } - - getGlobalVersion(data: any) { - const { path, method } = this.GLOBAL_VERSION; - const config = { - data, - }; - return this.makeRequest(path, method, config); - } - - getUpdate() { - const { path, method } = this.GLOBAL_UPDATE; - - return this.makeRequest(path, method); - } - - // Filtering - FILTERING_STATUS = { path: 'filtering/status', method: 'GET' }; - - FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' }; - - FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' }; - - FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' }; - - FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' }; - - FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' }; - - FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' }; - - FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' }; - - getFilteringStatus() { - const { path, method } = this.FILTERING_STATUS; - - return this.makeRequest(path, method); - } - - refreshFilters(config: any) { - const { path, method } = this.FILTERING_REFRESH; - const parameters = { - data: config, - }; - - return this.makeRequest(path, method, parameters); - } - - addFilter(config: any) { - const { path, method } = this.FILTERING_ADD_FILTER; - const parameters = { - data: config, - }; - - return this.makeRequest(path, method, parameters); - } - - removeFilter(config: any) { - const { path, method } = this.FILTERING_REMOVE_FILTER; - const parameters = { - data: config, - }; - - return this.makeRequest(path, method, parameters); - } - - setRules(rules: any) { - const { path, method } = this.FILTERING_SET_RULES; - const parameters = { - data: rules, - }; - return this.makeRequest(path, method, parameters); - } - - setFiltersConfig(config: any) { - const { path, method } = this.FILTERING_CONFIG; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - setFilterUrl(config: any) { - const { path, method } = this.FILTERING_SET_URL; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - checkHost(params: any) { - const { path, method } = this.FILTERING_CHECK_HOST; - const url = getPathWithQueryString(path, params); - - return this.makeRequest(url, method); - } - - // Parental - PARENTAL_STATUS = { path: 'parental/status', method: 'GET' }; - - PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' }; - - PARENTAL_DISABLE = { path: 'parental/disable', method: 'POST' }; - - getParentalStatus() { - const { path, method } = this.PARENTAL_STATUS; - - return this.makeRequest(path, method); - } - - enableParentalControl() { - const { path, method } = this.PARENTAL_ENABLE; - - return this.makeRequest(path, method); - } - - disableParentalControl() { - const { path, method } = this.PARENTAL_DISABLE; - - return this.makeRequest(path, method); - } - - // Safebrowsing - SAFEBROWSING_STATUS = { path: 'safebrowsing/status', method: 'GET' }; - - SAFEBROWSING_ENABLE = { path: 'safebrowsing/enable', method: 'POST' }; - - SAFEBROWSING_DISABLE = { path: 'safebrowsing/disable', method: 'POST' }; - - getSafebrowsingStatus() { - const { path, method } = this.SAFEBROWSING_STATUS; - - return this.makeRequest(path, method); - } - - enableSafebrowsing() { - const { path, method } = this.SAFEBROWSING_ENABLE; - - return this.makeRequest(path, method); - } - - disableSafebrowsing() { - const { path, method } = this.SAFEBROWSING_DISABLE; - - return this.makeRequest(path, method); - } - - // Safesearch - SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' }; - - SAFESEARCH_UPDATE = { path: 'safesearch/settings', method: 'PUT' }; - - getSafesearchStatus() { - const { path, method } = this.SAFESEARCH_STATUS; - - return this.makeRequest(path, method); - } - - /** - * interface SafeSearchConfig { - "enabled": boolean, - "bing": boolean, - "duckduckgo": boolean, - "google": boolean, - "pixabay": boolean, - "yandex": boolean, - "youtube": boolean - * } - * @param {*} data - SafeSearchConfig - * @returns 200 ok - */ - updateSafesearch(data: any) { - const { path, method } = this.SAFESEARCH_UPDATE; - return this.makeRequest(path, method, { data }); - } - - // enableSafesearch() { - // const { path, method } = this.SAFESEARCH_ENABLE; - // return this.makeRequest(path, method); - // } - - // disableSafesearch() { - // const { path, method } = this.SAFESEARCH_DISABLE; - // return this.makeRequest(path, method); - // } - - // Language - async changeLanguage(config: { language: LocalesType }) { - const profile = await this.getProfile(); - profile.language = config.language; - - return this.setProfile(profile); - } - - // Theme - async changeTheme(config: { theme: Theme }) { - const profile = await this.getProfile(); - profile.theme = config.theme; - - return this.setProfile(profile); - } - - // DHCP - DHCP_STATUS = { path: 'dhcp/status', method: 'GET' }; - - DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' }; - - DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' }; - - DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' }; - - DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' }; - - DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' }; - - DHCP_UPDATE_STATIC_LEASE = { path: 'dhcp/update_static_lease', method: 'POST' }; - - DHCP_RESET = { path: 'dhcp/reset', method: 'POST' }; - - DHCP_LEASES_RESET = { path: 'dhcp/reset_leases', method: 'POST' }; - - getDhcpStatus() { - const { path, method } = this.DHCP_STATUS; - - return this.makeRequest(path, method); - } - - getDhcpInterfaces() { - const { path, method } = this.DHCP_INTERFACES; - - return this.makeRequest(path, method); - } - - setDhcpConfig(config: any) { - const { path, method } = this.DHCP_SET_CONFIG; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - findActiveDhcp(req: any) { - const { path, method } = this.DHCP_FIND_ACTIVE; - const parameters = { - data: req, - }; - return this.makeRequest(path, method, parameters); - } - - addStaticLease(config: any) { - const { path, method } = this.DHCP_ADD_STATIC_LEASE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - removeStaticLease(config: any) { - const { path, method } = this.DHCP_REMOVE_STATIC_LEASE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - updateStaticLease(config: any) { - const { path, method } = this.DHCP_UPDATE_STATIC_LEASE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - resetDhcp() { - const { path, method } = this.DHCP_RESET; - - return this.makeRequest(path, method); - } - - resetDhcpLeases() { - const { path, method } = this.DHCP_LEASES_RESET; - - return this.makeRequest(path, method); - } - - // Installation - INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' }; - - INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' }; - - INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' }; - - getDefaultAddresses() { - const { path, method } = this.INSTALL_GET_ADDRESSES; - - return this.makeRequest(path, method); - } - - setAllSettings(config: any) { - const { path, method } = this.INSTALL_CONFIGURE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - checkConfig(config: any) { - const { path, method } = this.INSTALL_CHECK_CONFIG; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // DNS-over-HTTPS and DNS-over-TLS - TLS_STATUS = { path: 'tls/status', method: 'GET' }; - - TLS_CONFIG = { path: 'tls/configure', method: 'POST' }; - - TLS_VALIDATE = { path: 'tls/validate', method: 'POST' }; - - getTlsStatus() { - const { path, method } = this.TLS_STATUS; - - return this.makeRequest(path, method); - } - - setTlsConfig(config: any) { - const { path, method } = this.TLS_CONFIG; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - validateTlsConfig(config: any) { - const { path, method } = this.TLS_VALIDATE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // Per-client settings - GET_CLIENTS = { path: 'clients', method: 'GET' }; - - SEARCH_CLIENTS = { path: 'clients/search', method: 'POST' }; - - ADD_CLIENT = { path: 'clients/add', method: 'POST' }; - - DELETE_CLIENT = { path: 'clients/delete', method: 'POST' }; - - UPDATE_CLIENT = { path: 'clients/update', method: 'POST' }; - - getClients() { - const { path, method } = this.GET_CLIENTS; - - return this.makeRequest(path, method); - } - - addClient(config: any) { - const { path, method } = this.ADD_CLIENT; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - deleteClient(config: any) { - const { path, method } = this.DELETE_CLIENT; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - updateClient(config: any) { - const { path, method } = this.UPDATE_CLIENT; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - searchClients(config: any) { - const { path, method } = this.SEARCH_CLIENTS; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // DNS access settings - ACCESS_LIST = { path: 'access/list', method: 'GET' }; - - ACCESS_SET = { path: 'access/set', method: 'POST' }; - - getAccessList() { - const { path, method } = this.ACCESS_LIST; - - return this.makeRequest(path, method); - } - - setAccessList(config: any) { - const { path, method } = this.ACCESS_SET; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // DNS rewrites - REWRITES_LIST = { path: 'rewrite/list', method: 'GET' }; - - REWRITE_ADD = { path: 'rewrite/add', method: 'POST' }; - - REWRITE_UPDATE = { path: 'rewrite/update', method: 'PUT' }; - - REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' }; - - getRewritesList() { - const { path, method } = this.REWRITES_LIST; - - return this.makeRequest(path, method); - } - - addRewrite(config: any) { - const { path, method } = this.REWRITE_ADD; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - updateRewrite(config: any) { - const { path, method } = this.REWRITE_UPDATE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - deleteRewrite(config: any) { - const { path, method } = this.REWRITE_DELETE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - REWRITE_SETTINGS = { path: 'rewrite/settings', method: 'GET' }; - - REWRITE_SETTINGS_UPDATE = { path: 'rewrite/settings/update', method: 'PUT' }; - - getRewriteSettings() { - const { path, method } = this.REWRITE_SETTINGS; - return this.makeRequest(path, method); - } - - updateRewriteSettings(config: { enabled: boolean }) { - const { path, method } = this.REWRITE_SETTINGS_UPDATE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // Blocked services - BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' }; - - BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' }; - - BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' }; - - getAllBlockedServices() { - const { path, method } = this.BLOCKED_SERVICES_ALL; - - return this.makeRequest(path, method); - } - - getBlockedServices() { - const { path, method } = this.BLOCKED_SERVICES_GET; - - return this.makeRequest(path, method); - } - - updateBlockedServices(config: any) { - const { path, method } = this.BLOCKED_SERVICES_UPDATE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); - } - - // Settings for statistics - GET_STATS = { path: 'stats', method: 'GET' }; - - GET_STATS_CONFIG = { path: 'stats/config', method: 'GET' }; - - UPDATE_STATS_CONFIG = { path: 'stats/config/update', method: 'PUT' }; - - STATS_RESET = { path: 'stats_reset', method: 'POST' }; - - getStats(recent?: number) { - const { path, method } = this.GET_STATS; - const config = recent ? { params: { recent } } : undefined; - - return this.makeRequest(path, method, config); - } - - getStatsConfig() { - const { path, method } = this.GET_STATS_CONFIG; - - return this.makeRequest(path, method); - } - - setStatsConfig(data: any) { - const { path, method } = this.UPDATE_STATS_CONFIG; - const config = { - data, - }; - return this.makeRequest(path, method, config); - } - - resetStats() { - const { path, method } = this.STATS_RESET; - - return this.makeRequest(path, method); - } - - // Query log - GET_QUERY_LOG = { path: 'querylog', method: 'GET' }; - - UPDATE_QUERY_LOG_CONFIG = { path: 'querylog/config/update', method: 'PUT' }; - - GET_QUERY_LOG_CONFIG = { path: 'querylog/config', method: 'GET' }; - - QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' }; - - getQueryLog(params: any) { - const { path, method } = this.GET_QUERY_LOG; - const url = getPathWithQueryString(path, { - ...params, - limit: QUERY_LOGS_PAGE_LIMIT, - }); - - return this.makeRequest(url, method); - } - - getQueryLogConfig() { - const { path, method } = this.GET_QUERY_LOG_CONFIG; - - return this.makeRequest(path, method); - } - - setQueryLogConfig(data: any) { - const { path, method } = this.UPDATE_QUERY_LOG_CONFIG; - const config = { - data, - }; - return this.makeRequest(path, method, config); - } - - clearQueryLog() { - const { path, method } = this.QUERY_LOG_CLEAR; - - return this.makeRequest(path, method); - } - - // Login - LOGIN = { path: 'login', method: 'POST' }; - - login(data: any) { - const { path, method } = this.LOGIN; - const config = { - data, - }; - return this.makeRequest(path, method, config); - } - - // Logout - LOGOUT_PATH = 'logout'; - - getLogoutUrl() { - return `${this.baseUrl}/${this.LOGOUT_PATH}`; - } - - // Profile - GET_PROFILE = { path: 'profile', method: 'GET' }; - - UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' }; - - getProfile() { - const { path, method } = this.GET_PROFILE; - - return this.makeRequest(path, method); - } - - setProfile(data: any) { - const theme = data.theme ? data.theme : THEMES.auto; - const defaultLanguage = intl.getUILanguage() ? intl.getUILanguage() : 'en'; - const language = data.language ? data.language : defaultLanguage; - - const { path, method } = this.UPDATE_PROFILE; - const config = { data: { theme, language } }; - - return this.makeRequest(path, method, config); - } - - // DNS config - GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' }; - - SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' }; - - getDnsConfig() { - const { path, method } = this.GET_DNS_CONFIG; - - return this.makeRequest(path, method); - } - - setDnsConfig(data: any) { - const { path, method } = this.SET_DNS_CONFIG; - const config = { - data, - }; - return this.makeRequest(path, method, config); - } - - SET_PROTECTION = { path: 'protection', method: 'POST' }; - - setProtection(data: any) { - const { enabled, duration } = data; - const { path, method } = this.SET_PROTECTION; - - return this.makeRequest(path, method, { data: { enabled, duration } }); - } - - // Cache - CLEAR_CACHE = { path: 'cache_clear', method: 'POST' }; - - clearCache() { - const { path, method } = this.CLEAR_CACHE; - - return this.makeRequest(path, method); - } -} - -export const apiClient = new Api(BASE_URL); diff --git a/client_v2/src/api/customFetch.ts b/client_v2/src/api/customFetch.ts new file mode 100644 index 000000000..ea4850558 --- /dev/null +++ b/client_v2/src/api/customFetch.ts @@ -0,0 +1,78 @@ +import { HTML_PAGES, R_PATH_LAST_PART } from '../helpers/constants'; + +type CustomFetchOptions = RequestInit & { + skipAuthRedirect?: boolean; +}; + +/** + * Custom fetch mutator for orval-generated API client. + * + * Custom fetch wrapper providing AdGuard Home-specific behaviors: + * - Sets `Content-Type: application/json` on requests with a body + * - Parses response as JSON (falls back to raw text if JSON.parse fails) + * - On 403 and not on login/install page → redirects browser to login + * and returns `false` (does not throw) + * - On 403 while on login/install page → throws Error normally + * - Throws Error with format `${path} | ${data} | ${status}` for all + * other non-ok responses + * - On empty response body → returns empty string '' + * - On 204 → returns empty string '' + */ +export const customFetch = async (url: string, options?: CustomFetchOptions): Promise => { + const { skipAuthRedirect, ...fetchOptions } = options || {}; + + const fullUrl = url; + const headers: Record = {}; + + // Preserve any headers passed in options + if (fetchOptions.headers) { + const incomingHeaders = fetchOptions.headers as Record; + Object.assign(headers, incomingHeaders); + } + + try { + const response = await fetch(fullUrl, { + ...fetchOptions, + headers, + }); + + const text = await response.text(); + const data: T = text + ? (() => { + try { + return JSON.parse(text); + } catch { + return text; + } + })() + : ('' as unknown as T); + + if (!response.ok) { + const { pathname } = document.location; + const shouldRedirect = pathname !== HTML_PAGES.LOGIN && pathname !== HTML_PAGES.INSTALL; + + if (response.status === 403 && shouldRedirect && !skipAuthRedirect) { + const loginPageUrl = window.location.href.replace( + R_PATH_LAST_PART, + HTML_PAGES.LOGIN, + ); + window.location.replace(loginPageUrl); + return false as unknown as T; + } + + throw new Error( + `${fullUrl} | ${typeof data === 'string' ? data : JSON.stringify(data)} | ${response.status}`, + ); + } + + return data; + } catch (error) { + if (error instanceof Error && error.message.includes('|')) { + throw error; + } + + throw new Error(`${fullUrl} | ${error instanceof Error ? error.message : String(error)}`); + } +}; + +export default customFetch; diff --git a/client_v2/src/api/generated.ts b/client_v2/src/api/generated.ts new file mode 100644 index 000000000..8c2f55fdb --- /dev/null +++ b/client_v2/src/api/generated.ts @@ -0,0 +1,1513 @@ +import type { + AccessList, + AddUrlRequest, + AddressesInfo, + BlockedServicesAll, + BlockedServicesArray, + BlockedServicesSchedule, + CheckConfigRequest, + CheckConfigResponse, + Client, + ClientDelete, + ClientUpdate, + Clients, + ClientsFindParams, + ClientsFindResponse, + ClientsSearchRequest, + DHCPNetInterfaces, + DNSConfig, + DhcpConfig, + DhcpFindActiveReq, + DhcpSearchResult, + DhcpStaticLeaseBody, + DhcpStatus, + DnsInfo200, + FilterCheckHostResponse, + FilterConfig, + FilterRefreshRequest, + FilterRefreshResponse, + FilterSetUrl, + FilterStatus, + FilteringCheckHostParams, + GetQueryLogConfigResponse, + GetStatsConfigResponse, + GetVersionRequest, + InitialConfiguration, + LanguageSettings, + Login, + MobileConfigDoHParams, + MobileConfigDoTParams, + ParentalStatus200, + ProfileInfo, + QueryLog, + QueryLogConfig, + QueryLogParams, + RemoveUrlRequest, + RewriteEntryBody, + RewriteList, + RewriteSettings, + RewriteSettingsBody, + RewriteUpdateBody, + SafeSearchConfig, + SafebrowsingStatus200, + ServerStatus, + SetProtectionRequest, + SetRulesRequest, + Stats, + StatsConfig, + StatsParams, + TlsConfig, + TlsConfigBody, + UpstreamsConfig, + UpstreamsConfigResponse, + VersionInfo, +} from './model'; + +import { customFetch } from './customFetch'; +export const getStatusUrl = () => { + return `control/status`; +}; + +/** + * @summary Get DNS server current status and general settings + */ +export const status = async (options?: RequestInit): Promise => { + return customFetch(getStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getDnsInfoUrl = () => { + return `control/dns_info`; +}; + +/** + * @summary Get general DNS parameters + */ +export const dnsInfo = async (options?: RequestInit): Promise => { + return customFetch(getDnsInfoUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getDnsConfigUrl = () => { + return `control/dns_config`; +}; + +/** + * @summary Set general DNS parameters + */ +export const dnsConfig = async (dNSConfig?: DNSConfig, options?: RequestInit): Promise => { + return customFetch(getDnsConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dNSConfig), + }); +}; + +export const getSetProtectionUrl = () => { + return `control/protection`; +}; + +/** + * @summary Set protection state and duration + */ +export const setProtection = async ( + setProtectionRequest?: SetProtectionRequest, + options?: RequestInit, +): Promise => { + return customFetch(getSetProtectionUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(setProtectionRequest), + }); +}; + +export const getCacheClearUrl = () => { + return `control/cache_clear`; +}; + +/** + * @summary Clear DNS cache + */ +export const cacheClear = async (options?: RequestInit): Promise => { + return customFetch(getCacheClearUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getTestUpstreamDNSUrl = () => { + return `control/test_upstream_dns`; +}; + +/** + * @summary Test upstream configuration + */ +export const testUpstreamDNS = async ( + upstreamsConfig?: UpstreamsConfig, + options?: RequestInit, +): Promise => { + return customFetch(getTestUpstreamDNSUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(upstreamsConfig), + }); +}; + +export const getGetVersionJsonUrl = () => { + return `control/version.json`; +}; + +/** + * @summary Gets information about the latest available version of AdGuard + + */ +export const getVersionJson = async ( + getVersionRequest: GetVersionRequest, + options?: RequestInit, +): Promise => { + return customFetch(getGetVersionJsonUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(getVersionRequest), + }); +}; + +export const getBeginUpdateUrl = () => { + return `control/update`; +}; + +/** + * @summary Begin auto-upgrade procedure + */ +export const beginUpdate = async (options?: RequestInit): Promise => { + return customFetch(getBeginUpdateUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getQueryLogUrl = (params?: QueryLogParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + const explodeParameters = ['reason']; + + if (Array.isArray(value) && explodeParameters.includes(key)) { + value.forEach((v) => { + normalizedParams.append(key, v === null ? 'null' : String(v)); + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `control/querylog?${stringifiedParams}` + : `control/querylog`; +}; + +/** + * @summary Get DNS server query log. + */ +export const queryLog = async ( + params?: QueryLogParams, + options?: RequestInit, +): Promise => { + return customFetch(getQueryLogUrl(params), { + ...options, + method: 'GET', + }); +}; + +export const getQueryLogInfoUrl = () => { + return `control/querylog_info`; +}; + +/** + * Deprecated: Use `GET /querylog/config` instead. + * + * NOTE: If `interval` was configured by editing configuration file or new + * HTTP API call `PUT /querylog/config/update` and it's not equal to + * previous allowed enum values then it will be equal to `90` days for + * compatibility reasons. + * @deprecated + * @summary Get query log parameters + */ +export const queryLogInfo = async (options?: RequestInit): Promise => { + return customFetch(getQueryLogInfoUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getQueryLogConfigUrl = () => { + return `control/querylog_config`; +}; + +/** + * Deprecated: Use `PUT /querylog/config/update` instead. + * @deprecated + * @summary Set query log parameters + */ +export const queryLogConfig = async ( + queryLogConfig?: QueryLogConfig, + options?: RequestInit, +): Promise => { + return customFetch(getQueryLogConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(queryLogConfig), + }); +}; + +export const getQuerylogClearUrl = () => { + return `control/querylog_clear`; +}; + +/** + * @summary Clear query log + */ +export const querylogClear = async (options?: RequestInit): Promise => { + return customFetch(getQuerylogClearUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getGetQueryLogConfigUrl = () => { + return `control/querylog/config`; +}; + +/** + * @summary Get query log parameters + */ +export const getQueryLogConfig = async ( + options?: RequestInit, +): Promise => { + return customFetch(getGetQueryLogConfigUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getPutQueryLogConfigUrl = () => { + return `control/querylog/config/update`; +}; + +/** + * @summary Set query log parameters + */ +export const putQueryLogConfig = async ( + getQueryLogConfigResponse: GetQueryLogConfigResponse, + options?: RequestInit, +): Promise => { + return customFetch(getPutQueryLogConfigUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(getQueryLogConfigResponse), + }); +}; + +export const getStatsUrl = (params?: StatsParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `control/stats?${stringifiedParams}` : `control/stats`; +}; + +/** + * @summary Get DNS server statistics + */ +export const stats = async (params?: StatsParams, options?: RequestInit): Promise => { + return customFetch(getStatsUrl(params), { + ...options, + method: 'GET', + }); +}; + +export const getStatsResetUrl = () => { + return `control/stats_reset`; +}; + +/** + * @summary Reset all statistics to zeroes + */ +export const statsReset = async (options?: RequestInit): Promise => { + return customFetch(getStatsResetUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getStatsInfoUrl = () => { + return `control/stats_info`; +}; + +/** + * Deprecated: Use `GET /stats/config` instead. + * + * NOTE: If `interval` was configured by editing configuration file or new + * HTTP API call `PUT /stats/config/update` and it's not equal to + * previous allowed enum values then it will be equal to `90` days for + * compatibility reasons. + * @deprecated + * @summary Get statistics parameters + */ +export const statsInfo = async (options?: RequestInit): Promise => { + return customFetch(getStatsInfoUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getStatsConfigUrl = () => { + return `control/stats_config`; +}; + +/** + * Deprecated: Use `PUT /stats/config/update` instead. + * @deprecated + * @summary Set statistics parameters + */ +export const statsConfig = async ( + statsConfig?: StatsConfig, + options?: RequestInit, +): Promise => { + return customFetch(getStatsConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(statsConfig), + }); +}; + +export const getGetStatsConfigUrl = () => { + return `control/stats/config`; +}; + +/** + * @summary Get statistics parameters + */ +export const getStatsConfig = async (options?: RequestInit): Promise => { + return customFetch(getGetStatsConfigUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getPutStatsConfigUrl = () => { + return `control/stats/config/update`; +}; + +/** + * @summary Set statistics parameters + */ +export const putStatsConfig = async ( + getStatsConfigResponse: GetStatsConfigResponse, + options?: RequestInit, +): Promise => { + return customFetch(getPutStatsConfigUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(getStatsConfigResponse), + }); +}; + +export const getTlsStatusUrl = () => { + return `control/tls/status`; +}; + +/** + * @summary Returns TLS configuration and its status + */ +export const tlsStatus = async (options?: RequestInit): Promise => { + return customFetch(getTlsStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getTlsConfigureUrl = () => { + return `control/tls/configure`; +}; + +/** + * @summary Updates current TLS configuration + */ +export const tlsConfigure = async ( + tlsConfigBody: TlsConfigBody, + options?: RequestInit, +): Promise => { + return customFetch(getTlsConfigureUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(tlsConfigBody), + }); +}; + +export const getTlsValidateUrl = () => { + return `control/tls/validate`; +}; + +/** + * @summary Checks if the current TLS configuration is valid + */ +export const tlsValidate = async ( + tlsConfigBody: TlsConfigBody, + options?: RequestInit, +): Promise => { + return customFetch(getTlsValidateUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(tlsConfigBody), + }); +}; + +export const getDhcpStatusUrl = () => { + return `control/dhcp/status`; +}; + +/** + * @summary Gets the current DHCP settings and status + */ +export const dhcpStatus = async (options?: RequestInit): Promise => { + return customFetch(getDhcpStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getDhcpInterfacesUrl = () => { + return `control/dhcp/interfaces`; +}; + +/** + * @summary Gets the available interfaces + */ +export const dhcpInterfaces = async (options?: RequestInit): Promise => { + return customFetch(getDhcpInterfacesUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getDhcpSetConfigUrl = () => { + return `control/dhcp/set_config`; +}; + +/** + * @summary Updates the current DHCP server configuration + */ +export const dhcpSetConfig = async ( + dhcpConfig?: DhcpConfig, + options?: RequestInit, +): Promise => { + return customFetch(getDhcpSetConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dhcpConfig), + }); +}; + +export const getCheckActiveDhcpUrl = () => { + return `control/dhcp/find_active_dhcp`; +}; + +/** + * @summary Searches for an active DHCP server on the network + */ +export const checkActiveDhcp = async ( + dhcpFindActiveReq?: DhcpFindActiveReq, + options?: RequestInit, +): Promise => { + return customFetch(getCheckActiveDhcpUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dhcpFindActiveReq), + }); +}; + +export const getDhcpAddStaticLeaseUrl = () => { + return `control/dhcp/add_static_lease`; +}; + +/** + * @summary Adds a static lease + */ +export const dhcpAddStaticLease = async ( + dhcpStaticLeaseBody: DhcpStaticLeaseBody, + options?: RequestInit, +): Promise => { + return customFetch(getDhcpAddStaticLeaseUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dhcpStaticLeaseBody), + }); +}; + +export const getDhcpRemoveStaticLeaseUrl = () => { + return `control/dhcp/remove_static_lease`; +}; + +/** + * @summary Removes a static lease + */ +export const dhcpRemoveStaticLease = async ( + dhcpStaticLeaseBody: DhcpStaticLeaseBody, + options?: RequestInit, +): Promise => { + return customFetch(getDhcpRemoveStaticLeaseUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dhcpStaticLeaseBody), + }); +}; + +export const getDhcpUpdateStaticLeaseUrl = () => { + return `control/dhcp/update_static_lease`; +}; + +/** + * Updates IP address, hostname of the static lease. IP version must be the same as previous. + * @summary Updates a static lease + */ +export const dhcpUpdateStaticLease = async ( + dhcpStaticLeaseBody: DhcpStaticLeaseBody, + options?: RequestInit, +): Promise => { + return customFetch(getDhcpUpdateStaticLeaseUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(dhcpStaticLeaseBody), + }); +}; + +export const getDhcpResetUrl = () => { + return `control/dhcp/reset`; +}; + +/** + * @summary Reset DHCP configuration + */ +export const dhcpReset = async (options?: RequestInit): Promise => { + return customFetch(getDhcpResetUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getDhcpResetLeasesUrl = () => { + return `control/dhcp/reset_leases`; +}; + +/** + * @summary Reset DHCP leases + */ +export const dhcpResetLeases = async (options?: RequestInit): Promise => { + return customFetch(getDhcpResetLeasesUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getFilteringStatusUrl = () => { + return `control/filtering/status`; +}; + +/** + * @summary Get filtering parameters + */ +export const filteringStatus = async (options?: RequestInit): Promise => { + return customFetch(getFilteringStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getFilteringConfigUrl = () => { + return `control/filtering/config`; +}; + +/** + * @summary Set filtering parameters + */ +export const filteringConfig = async ( + filterConfig: FilterConfig, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(filterConfig), + }); +}; + +export const getFilteringAddURLUrl = () => { + return `control/filtering/add_url`; +}; + +/** + * @summary Add filter URL or an absolute file path + */ +export const filteringAddURL = async ( + addUrlRequest: AddUrlRequest, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringAddURLUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(addUrlRequest), + }); +}; + +export const getFilteringRemoveURLUrl = () => { + return `control/filtering/remove_url`; +}; + +/** + * @summary Remove filter URL + */ +export const filteringRemoveURL = async ( + removeUrlRequest: RemoveUrlRequest, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringRemoveURLUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(removeUrlRequest), + }); +}; + +export const getFilteringSetURLUrl = () => { + return `control/filtering/set_url`; +}; + +/** + * @summary Set URL parameters + */ +export const filteringSetURL = async ( + filterSetUrl?: FilterSetUrl, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringSetURLUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(filterSetUrl), + }); +}; + +export const getFilteringRefreshUrl = () => { + return `control/filtering/refresh`; +}; + +/** + * @summary Reload filtering rules from URLs. This might be needed if new URL was just added and you don't want to wait for automatic refresh to kick in. This API request is ratelimited, so you can call it freely as often as you like, it wont create unnecessary burden on servers that host the URL. This should work as intended, a `force` parameter is offered as last-resort attempt to make filter lists fresh. If you ever find yourself using `force` to make something work that otherwise wont, this is a bug and report it accordingly. + + */ +export const filteringRefresh = async ( + filterRefreshRequest?: FilterRefreshRequest, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringRefreshUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(filterRefreshRequest), + }); +}; + +export const getFilteringSetRulesUrl = () => { + return `control/filtering/set_rules`; +}; + +/** + * @summary Set user-defined filter rules + */ +export const filteringSetRules = async ( + setRulesRequest?: SetRulesRequest, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringSetRulesUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(setRulesRequest), + }); +}; + +export const getFilteringCheckHostUrl = (params: FilteringCheckHostParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `control/filtering/check_host?${stringifiedParams}` + : `control/filtering/check_host`; +}; + +/** + * @summary Check if host name is filtered + */ +export const filteringCheckHost = async ( + params: FilteringCheckHostParams, + options?: RequestInit, +): Promise => { + return customFetch(getFilteringCheckHostUrl(params), { + ...options, + method: 'GET', + }); +}; + +export const getSafebrowsingEnableUrl = () => { + return `control/safebrowsing/enable`; +}; + +/** + * @summary Enable safebrowsing + */ +export const safebrowsingEnable = async (options?: RequestInit): Promise => { + return customFetch(getSafebrowsingEnableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getSafebrowsingDisableUrl = () => { + return `control/safebrowsing/disable`; +}; + +/** + * @summary Disable safebrowsing + */ +export const safebrowsingDisable = async (options?: RequestInit): Promise => { + return customFetch(getSafebrowsingDisableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getSafebrowsingStatusUrl = () => { + return `control/safebrowsing/status`; +}; + +/** + * @summary Get safebrowsing status + */ +export const safebrowsingStatus = async (options?: RequestInit): Promise => { + return customFetch(getSafebrowsingStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getParentalEnableUrl = () => { + return `control/parental/enable`; +}; + +/** + * @summary Enable parental filtering + */ +export const parentalEnable = async (options?: RequestInit): Promise => { + return customFetch(getParentalEnableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getParentalDisableUrl = () => { + return `control/parental/disable`; +}; + +/** + * @summary Disable parental filtering + */ +export const parentalDisable = async (options?: RequestInit): Promise => { + return customFetch(getParentalDisableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getParentalStatusUrl = () => { + return `control/parental/status`; +}; + +/** + * @summary Get parental filtering status + */ +export const parentalStatus = async (options?: RequestInit): Promise => { + return customFetch(getParentalStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getSafesearchEnableUrl = () => { + return `control/safesearch/enable`; +}; + +/** + * @deprecated + * @summary Enable safesearch + */ +export const safesearchEnable = async (options?: RequestInit): Promise => { + return customFetch(getSafesearchEnableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getSafesearchDisableUrl = () => { + return `control/safesearch/disable`; +}; + +/** + * @deprecated + * @summary Disable safesearch + */ +export const safesearchDisable = async (options?: RequestInit): Promise => { + return customFetch(getSafesearchDisableUrl(), { + ...options, + method: 'POST', + }); +}; + +export const getSafesearchSettingsUrl = () => { + return `control/safesearch/settings`; +}; + +/** + * @summary Update safesearch settings + */ +export const safesearchSettings = async ( + safeSearchConfig?: SafeSearchConfig, + options?: RequestInit, +): Promise => { + return customFetch(getSafesearchSettingsUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(safeSearchConfig), + }); +}; + +export const getSafesearchStatusUrl = () => { + return `control/safesearch/status`; +}; + +/** + * @summary Get safesearch status + */ +export const safesearchStatus = async (options?: RequestInit): Promise => { + return customFetch(getSafesearchStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getClientsStatusUrl = () => { + return `control/clients`; +}; + +/** + * @summary Get information about configured clients + */ +export const clientsStatus = async (options?: RequestInit): Promise => { + return customFetch(getClientsStatusUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getClientsAddUrl = () => { + return `control/clients/add`; +}; + +/** + * @summary Add a new client + */ +export const clientsAdd = async (client: Client, options?: RequestInit): Promise => { + return customFetch(getClientsAddUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(client), + }); +}; + +export const getClientsDeleteUrl = () => { + return `control/clients/delete`; +}; + +/** + * @summary Remove a client + */ +export const clientsDelete = async ( + clientDelete: ClientDelete, + options?: RequestInit, +): Promise => { + return customFetch(getClientsDeleteUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(clientDelete), + }); +}; + +export const getClientsUpdateUrl = () => { + return `control/clients/update`; +}; + +/** + * @summary Update client information + */ +export const clientsUpdate = async ( + clientUpdate: ClientUpdate, + options?: RequestInit, +): Promise => { + return customFetch(getClientsUpdateUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(clientUpdate), + }); +}; + +export const getClientsFindUrl = (params?: ClientsFindParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `control/clients/find?${stringifiedParams}` + : `control/clients/find`; +}; + +/** + * Deprecated: Use `POST /clients/search` instead. + * @deprecated + * @summary Get information about clients by their IP addresses or ClientIDs. + + */ +export const clientsFind = async ( + params?: ClientsFindParams, + options?: RequestInit, +): Promise => { + return customFetch(getClientsFindUrl(params), { + ...options, + method: 'GET', + }); +}; + +export const getClientsSearchUrl = () => { + return `control/clients/search`; +}; + +/** + * @summary Retrieve information about clients by performing an exact match search using IP addresses, CIDRs, MAC addresses, or ClientIDs. + + */ +export const clientsSearch = async ( + clientsSearchRequest: ClientsSearchRequest, + options?: RequestInit, +): Promise => { + return customFetch(getClientsSearchUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(clientsSearchRequest), + }); +}; + +export const getAccessListUrl = () => { + return `control/access/list`; +}; + +/** + * @summary List (dis)allowed clients, blocked hosts, etc. + */ +export const accessList = async (options?: RequestInit): Promise => { + return customFetch(getAccessListUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getAccessSetUrl = () => { + return `control/access/set`; +}; + +/** + * @summary Set (dis)allowed clients, blocked hosts, etc. + */ +export const accessSet = async (accessList: AccessList, options?: RequestInit): Promise => { + return customFetch(getAccessSetUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(accessList), + }); +}; + +export const getBlockedServicesAvailableServicesUrl = () => { + return `control/blocked_services/services`; +}; + +/** + * Deprecated: Use `GET /blocked_services/all` instead. + * @deprecated + * @summary Get available services to use for blocking + */ +export const blockedServicesAvailableServices = async ( + options?: RequestInit, +): Promise => { + return customFetch(getBlockedServicesAvailableServicesUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getBlockedServicesAllUrl = () => { + return `control/blocked_services/all`; +}; + +/** + * @summary Get available services to use for blocking + */ +export const blockedServicesAll = async (options?: RequestInit): Promise => { + return customFetch(getBlockedServicesAllUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getBlockedServicesListUrl = () => { + return `control/blocked_services/list`; +}; + +/** + * Deprecated: Use `GET /blocked_services/get` instead. + * @deprecated + * @summary Get blocked services list + */ +export const blockedServicesList = async (options?: RequestInit): Promise => { + return customFetch(getBlockedServicesListUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getBlockedServicesSetUrl = () => { + return `control/blocked_services/set`; +}; + +/** + * Deprecated: Use `PUT /blocked_services/update` instead. + * @deprecated + * @summary Set blocked services list + */ +export const blockedServicesSet = async ( + blockedServicesArray?: BlockedServicesArray, + options?: RequestInit, +): Promise => { + return customFetch(getBlockedServicesSetUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(blockedServicesArray), + }); +}; + +export const getBlockedServicesScheduleUrl = () => { + return `control/blocked_services/get`; +}; + +/** + * @summary Get blocked services + */ +export const blockedServicesSchedule = async ( + options?: RequestInit, +): Promise => { + return customFetch(getBlockedServicesScheduleUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getBlockedServicesScheduleUpdateUrl = () => { + return `control/blocked_services/update`; +}; + +/** + * @summary Update blocked services + */ +export const blockedServicesScheduleUpdate = async ( + blockedServicesSchedule: BlockedServicesSchedule, + options?: RequestInit, +): Promise => { + return customFetch(getBlockedServicesScheduleUpdateUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(blockedServicesSchedule), + }); +}; + +export const getRewriteListUrl = () => { + return `control/rewrite/list`; +}; + +/** + * @summary Get list of Rewrite rules + */ +export const rewriteList = async (options?: RequestInit): Promise => { + return customFetch(getRewriteListUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getRewriteAddUrl = () => { + return `control/rewrite/add`; +}; + +/** + * @summary Add a new Rewrite rule + */ +export const rewriteAdd = async ( + rewriteEntryBody: RewriteEntryBody, + options?: RequestInit, +): Promise => { + return customFetch(getRewriteAddUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(rewriteEntryBody), + }); +}; + +export const getRewriteDeleteUrl = () => { + return `control/rewrite/delete`; +}; + +/** + * @summary Remove a Rewrite rule + */ +export const rewriteDelete = async ( + rewriteEntryBody: RewriteEntryBody, + options?: RequestInit, +): Promise => { + return customFetch(getRewriteDeleteUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(rewriteEntryBody), + }); +}; + +export const getRewriteSettingsGetUrl = () => { + return `control/rewrite/settings`; +}; + +/** + * @summary Get rewrite settings + */ +export const rewriteSettingsGet = async (options?: RequestInit): Promise => { + return customFetch(getRewriteSettingsGetUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getRewriteSettingsUpdateUrl = () => { + return `control/rewrite/settings/update`; +}; + +/** + * @summary Update rewrite settings + */ +export const rewriteSettingsUpdate = async ( + rewriteSettingsBody: RewriteSettingsBody, + options?: RequestInit, +): Promise => { + return customFetch(getRewriteSettingsUpdateUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(rewriteSettingsBody), + }); +}; + +export const getRewriteUpdateUrl = () => { + return `control/rewrite/update`; +}; + +/** + * @summary Update a Rewrite rule + */ +export const rewriteUpdate = async ( + rewriteUpdateBody: RewriteUpdateBody, + options?: RequestInit, +): Promise => { + return customFetch(getRewriteUpdateUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(rewriteUpdateBody), + }); +}; + +export const getChangeLanguageUrl = () => { + return `control/i18n/change_language`; +}; + +/** + * Deprecated: Use `PUT /control/profile` instead. + * @deprecated + * @summary Change current language. Argument must be an ISO 639-1 two-letter code. + + */ +export const changeLanguage = async ( + languageSettings?: LanguageSettings, + options?: RequestInit, +): Promise => { + return customFetch(getChangeLanguageUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(languageSettings), + }); +}; + +export const getCurrentLanguageUrl = () => { + return `control/i18n/current_language`; +}; + +/** + * Deprecated: Use `GET /control/profile` instead. + * @deprecated + * @summary Get currently set language. Result is ISO 639-1 two-letter code. Empty result means default language. + + */ +export const currentLanguage = async (options?: RequestInit): Promise => { + return customFetch(getCurrentLanguageUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getInstallGetAddressesUrl = () => { + return `control/install/get_addresses`; +}; + +/** + * @summary Gets the network interfaces information. + */ +export const installGetAddresses = async (options?: RequestInit): Promise => { + return customFetch(getInstallGetAddressesUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getInstallCheckConfigUrl = () => { + return `control/install/check_config`; +}; + +/** + * @summary Checks configuration + */ +export const installCheckConfig = async ( + checkConfigRequest: CheckConfigRequest, + options?: RequestInit, +): Promise => { + return customFetch(getInstallCheckConfigUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(checkConfigRequest), + }); +}; + +export const getInstallConfigureUrl = () => { + return `control/install/configure`; +}; + +/** + * @summary Applies the initial configuration. + */ +export const installConfigure = async ( + initialConfiguration: InitialConfiguration, + options?: RequestInit, +): Promise => { + return customFetch(getInstallConfigureUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(initialConfiguration), + }); +}; + +export const getLoginUrl = () => { + return `control/login`; +}; + +/** + * @summary Perform administrator log-in + */ +export const login = async (login: Login, options?: RequestInit): Promise => { + return customFetch(getLoginUrl(), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(login), + }); +}; + +export const getLogoutUrl = () => { + return `control/logout`; +}; + +/** + * @summary Perform administrator log-out + */ +export const logout = async (options?: RequestInit): Promise => { + return customFetch(getLogoutUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getUpdateProfileUrl = () => { + return `control/profile/update`; +}; + +/** + * @summary Updates current user info + */ +export const updateProfile = async ( + profileInfo?: ProfileInfo, + options?: RequestInit, +): Promise => { + return customFetch(getUpdateProfileUrl(), { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(profileInfo), + }); +}; + +export const getGetProfileUrl = () => { + return `control/profile`; +}; + +export const getProfile = async (options?: RequestInit): Promise => { + return customFetch(getGetProfileUrl(), { + ...options, + method: 'GET', + }); +}; + +export const getMobileConfigDoHUrl = (params: MobileConfigDoHParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `control/apple/doh.mobileconfig?${stringifiedParams}` + : `control/apple/doh.mobileconfig`; +}; + +/** + * @summary Get DNS over HTTPS .mobileconfig. + */ +export const mobileConfigDoH = async ( + params: MobileConfigDoHParams, + options?: RequestInit, +): Promise => { + return customFetch(getMobileConfigDoHUrl(params), { + ...options, + method: 'GET', + }); +}; + +export const getMobileConfigDoTUrl = (params: MobileConfigDoTParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : String(value)); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `control/apple/dot.mobileconfig?${stringifiedParams}` + : `control/apple/dot.mobileconfig`; +}; + +/** + * @summary Get DNS over TLS .mobileconfig. + */ +export const mobileConfigDoT = async ( + params: MobileConfigDoTParams, + options?: RequestInit, +): Promise => { + return customFetch(getMobileConfigDoTUrl(params), { + ...options, + method: 'GET', + }); +}; diff --git a/client_v2/src/api/model/accessList.ts b/client_v2/src/api/model/accessList.ts new file mode 100644 index 000000000..0e39f0ff2 --- /dev/null +++ b/client_v2/src/api/model/accessList.ts @@ -0,0 +1,11 @@ +/** + * Client and host access list. Each of the lists should contain only unique elements. In addition, allowed and disallowed lists cannot contain the same elements. + */ +export interface AccessList { + /** The allowlist of clients: IP addresses, CIDRs, or ClientIDs. */ + allowed_clients?: string[]; + /** The blocklist of clients: IP addresses, CIDRs, or ClientIDs. */ + disallowed_clients?: string[]; + /** The blocklist of hosts. */ + blocked_hosts?: string[]; +} diff --git a/client_v2/src/api/model/accessListResponse.ts b/client_v2/src/api/model/accessListResponse.ts new file mode 100644 index 000000000..159f0bdf2 --- /dev/null +++ b/client_v2/src/api/model/accessListResponse.ts @@ -0,0 +1,3 @@ +import type { AccessList } from './accessList'; + +export type AccessListResponse = AccessList; diff --git a/client_v2/src/api/model/accessSetRequest.ts b/client_v2/src/api/model/accessSetRequest.ts new file mode 100644 index 000000000..00de27245 --- /dev/null +++ b/client_v2/src/api/model/accessSetRequest.ts @@ -0,0 +1,3 @@ +import type { AccessList } from './accessList'; + +export type AccessSetRequest = AccessList; diff --git a/client_v2/src/api/model/addUrlRequest.ts b/client_v2/src/api/model/addUrlRequest.ts new file mode 100644 index 000000000..e8094527a --- /dev/null +++ b/client_v2/src/api/model/addUrlRequest.ts @@ -0,0 +1,9 @@ +/** + * /add_url request data + */ +export interface AddUrlRequest { + name?: string; + /** URL or an absolute path to the file containing filtering rules. */ + url?: string; + whitelist?: boolean; +} diff --git a/client_v2/src/api/model/addressInfo.ts b/client_v2/src/api/model/addressInfo.ts new file mode 100644 index 000000000..7ca33370c --- /dev/null +++ b/client_v2/src/api/model/addressInfo.ts @@ -0,0 +1,7 @@ +/** + * Port information + */ +export interface AddressInfo { + ip: string; + port: number; +} diff --git a/client_v2/src/api/model/addressesInfo.ts b/client_v2/src/api/model/addressesInfo.ts new file mode 100644 index 000000000..4395b3d1b --- /dev/null +++ b/client_v2/src/api/model/addressesInfo.ts @@ -0,0 +1,11 @@ +import type { NetInterfaces } from './netInterfaces'; + +/** + * AdGuard Home addresses configuration + */ +export interface AddressesInfo { + dns_port: number; + interfaces: NetInterfaces; + version: string; + web_port: number; +} diff --git a/client_v2/src/api/model/blockedService.ts b/client_v2/src/api/model/blockedService.ts new file mode 100644 index 000000000..0deba6aee --- /dev/null +++ b/client_v2/src/api/model/blockedService.ts @@ -0,0 +1,12 @@ +export interface BlockedService { + /** The SVG icon as a Base64-encoded string to make it easier to embed it into a data URL. */ + icon_svg: string; + /** The ID of this service. */ + id: string; + /** The human-readable name of this service. */ + name: string; + /** The array of the filtering rules. */ + rules: string[]; + /** The ID of the group, that the service belongs to. */ + group_id?: string; +} diff --git a/client_v2/src/api/model/blockedServicesAll.ts b/client_v2/src/api/model/blockedServicesAll.ts new file mode 100644 index 000000000..8b66f217b --- /dev/null +++ b/client_v2/src/api/model/blockedServicesAll.ts @@ -0,0 +1,7 @@ +import type { BlockedService } from './blockedService'; +import type { ServiceGroup } from './serviceGroup'; + +export interface BlockedServicesAll { + blocked_services: BlockedService[]; + groups: ServiceGroup[]; +} diff --git a/client_v2/src/api/model/blockedServicesArray.ts b/client_v2/src/api/model/blockedServicesArray.ts new file mode 100644 index 000000000..fcb1bb112 --- /dev/null +++ b/client_v2/src/api/model/blockedServicesArray.ts @@ -0,0 +1 @@ +export type BlockedServicesArray = string[]; diff --git a/client_v2/src/api/model/blockedServicesSchedule.ts b/client_v2/src/api/model/blockedServicesSchedule.ts new file mode 100644 index 000000000..ae91b472d --- /dev/null +++ b/client_v2/src/api/model/blockedServicesSchedule.ts @@ -0,0 +1,7 @@ +import type { Schedule } from './schedule'; + +export interface BlockedServicesSchedule { + schedule?: Schedule; + /** The names of the blocked services. */ + ids?: string[]; +} diff --git a/client_v2/src/api/model/checkConfigRequest.ts b/client_v2/src/api/model/checkConfigRequest.ts new file mode 100644 index 000000000..41cffd932 --- /dev/null +++ b/client_v2/src/api/model/checkConfigRequest.ts @@ -0,0 +1,12 @@ +import type { CheckConfigRequestInfo } from './checkConfigRequestInfo'; +import type { Lang } from './lang'; + +/** + * Configuration to be checked + */ +export interface CheckConfigRequest { + dns?: CheckConfigRequestInfo; + language?: Lang; + set_static_ip?: boolean; + web?: CheckConfigRequestInfo; +} diff --git a/client_v2/src/api/model/checkConfigRequestInfo.ts b/client_v2/src/api/model/checkConfigRequestInfo.ts new file mode 100644 index 000000000..a75fea29b --- /dev/null +++ b/client_v2/src/api/model/checkConfigRequestInfo.ts @@ -0,0 +1,5 @@ +export interface CheckConfigRequestInfo { + ip?: string; + port?: number; + autofix?: boolean; +} diff --git a/client_v2/src/api/model/checkConfigResponse.ts b/client_v2/src/api/model/checkConfigResponse.ts new file mode 100644 index 000000000..55033b666 --- /dev/null +++ b/client_v2/src/api/model/checkConfigResponse.ts @@ -0,0 +1,9 @@ +import type { CheckConfigResponseInfo } from './checkConfigResponseInfo'; +import type { CheckConfigStaticIpInfo } from './checkConfigStaticIpInfo'; + +export interface CheckConfigResponse { + dns: CheckConfigResponseInfo; + language: CheckConfigResponseInfo; + static_ip: CheckConfigStaticIpInfo; + web: CheckConfigResponseInfo; +} diff --git a/client_v2/src/api/model/checkConfigResponseInfo.ts b/client_v2/src/api/model/checkConfigResponseInfo.ts new file mode 100644 index 000000000..92cb314e6 --- /dev/null +++ b/client_v2/src/api/model/checkConfigResponseInfo.ts @@ -0,0 +1,4 @@ +export interface CheckConfigResponseInfo { + status: string; + can_autofix: boolean; +} diff --git a/client_v2/src/api/model/checkConfigStaticIpInfo.ts b/client_v2/src/api/model/checkConfigStaticIpInfo.ts new file mode 100644 index 000000000..e6d48b190 --- /dev/null +++ b/client_v2/src/api/model/checkConfigStaticIpInfo.ts @@ -0,0 +1,9 @@ +import type { CheckConfigStaticIpInfoStatic } from './checkConfigStaticIpInfoStatic'; + +export interface CheckConfigStaticIpInfo { + static?: CheckConfigStaticIpInfoStatic; + /** Current dynamic IP address. Set if static=no */ + ip?: string; + /** Error text. Set if static=error */ + error?: string; +} diff --git a/client_v2/src/api/model/checkConfigStaticIpInfoStatic.ts b/client_v2/src/api/model/checkConfigStaticIpInfoStatic.ts new file mode 100644 index 000000000..23b26dab7 --- /dev/null +++ b/client_v2/src/api/model/checkConfigStaticIpInfoStatic.ts @@ -0,0 +1,4 @@ +/** + * Can be: yes, no, error + */ +export type CheckConfigStaticIpInfoStatic = 'yes' | 'no' | 'error'; diff --git a/client_v2/src/api/model/client.ts b/client_v2/src/api/model/client.ts new file mode 100644 index 000000000..7542acb96 --- /dev/null +++ b/client_v2/src/api/model/client.ts @@ -0,0 +1,63 @@ +import type { SafeSearchConfig } from './safeSearchConfig'; +import type { Schedule } from './schedule'; + +/** + * Client information. + */ +export interface Client { + /** Name */ + name?: string; + /** IP, CIDR, MAC, or ClientID. */ + ids?: string[]; + use_global_settings?: boolean; + filtering_enabled?: boolean; + parental_enabled?: boolean; + safebrowsing_enabled?: boolean; + /** @deprecated */ + safesearch_enabled?: boolean; + safe_search?: SafeSearchConfig; + use_global_blocked_services?: boolean; + blocked_services_schedule?: Schedule; + blocked_services?: string[]; + upstreams?: string[]; + tags?: string[]; + /** + * NOTE: If `ignore_querylog` is not set in HTTP API `GET /clients/add` + * request then default value (false) will be used. + * + * If `ignore_querylog` is not set in HTTP API `GET /clients/update` + * request then the existing value will not be changed. + * + * This behaviour can be changed in the future versions. + */ + ignore_querylog?: boolean; + /** + * NOTE: If `ignore_statistics` is not set in HTTP API `GET + * /clients/add` request then default value (false) will be used. + * + * If `ignore_statistics` is not set in HTTP API `GET /clients/update` + * request then the existing value will not be changed. + * + * This behaviour can be changed in the future versions. + */ + ignore_statistics?: boolean; + /** + * NOTE: If `upstreams_cache_enabled` is not set in HTTP API + * `GET /clients/add` request then default value (false) will be used. + * + * If `upstreams_cache_enabled` is not set in HTTP API + * `GET /clients/update` request then the existing value will not be + * changed. + * + * This behaviour can be changed in the future versions. + */ + upstreams_cache_enabled?: boolean; + /** + * NOTE: If `upstreams_cache_enabled` is not set in HTTP API + * `GET /clients/update` request then the existing value will not be + * changed. + * + * This behaviour can be changed in the future versions. + */ + upstreams_cache_size?: number; +} diff --git a/client_v2/src/api/model/clientAuto.ts b/client_v2/src/api/model/clientAuto.ts new file mode 100644 index 000000000..c4073e941 --- /dev/null +++ b/client_v2/src/api/model/clientAuto.ts @@ -0,0 +1,14 @@ +import type { WhoisInfo } from './whoisInfo'; + +/** + * Auto-Client information + */ +export interface ClientAuto { + /** IP address */ + ip?: string; + /** Name */ + name?: string; + /** The source of this information */ + source?: string; + whois_info?: WhoisInfo; +} diff --git a/client_v2/src/api/model/clientDelete.ts b/client_v2/src/api/model/clientDelete.ts new file mode 100644 index 000000000..fd2323d1d --- /dev/null +++ b/client_v2/src/api/model/clientDelete.ts @@ -0,0 +1,6 @@ +/** + * Client delete request + */ +export interface ClientDelete { + name?: string; +} diff --git a/client_v2/src/api/model/clientFindSubEntry.ts b/client_v2/src/api/model/clientFindSubEntry.ts new file mode 100644 index 000000000..4473c90bf --- /dev/null +++ b/client_v2/src/api/model/clientFindSubEntry.ts @@ -0,0 +1,29 @@ +import type { SafeSearchConfig } from './safeSearchConfig'; +import type { WhoisInfo } from './whoisInfo'; + +/** + * Client information. + */ +export interface ClientFindSubEntry { + /** Name */ + name?: string; + /** IP, CIDR, MAC, or ClientID. */ + ids?: string[]; + use_global_settings?: boolean; + filtering_enabled?: boolean; + parental_enabled?: boolean; + safebrowsing_enabled?: boolean; + /** @deprecated */ + safesearch_enabled?: boolean; + safe_search?: SafeSearchConfig; + use_global_blocked_services?: boolean; + blocked_services?: string[]; + upstreams?: string[]; + whois_info?: WhoisInfo; + /** Whether the client's IP is blocked or not. */ + disallowed?: boolean; + /** The rule due to which the client is disallowed. If disallowed is set to true, and this string is empty, then the client IP is disallowed by the "allowed IP list", that is it is not included in the allowed list. */ + disallowed_rule?: string; + ignore_querylog?: boolean; + ignore_statistics?: boolean; +} diff --git a/client_v2/src/api/model/clientUpdate.ts b/client_v2/src/api/model/clientUpdate.ts new file mode 100644 index 000000000..c1bc7b263 --- /dev/null +++ b/client_v2/src/api/model/clientUpdate.ts @@ -0,0 +1,9 @@ +import type { Client } from './client'; + +/** + * Client update request + */ +export interface ClientUpdate { + name?: string; + data?: Client; +} diff --git a/client_v2/src/api/model/clients.ts b/client_v2/src/api/model/clients.ts new file mode 100644 index 000000000..91b93387f --- /dev/null +++ b/client_v2/src/api/model/clients.ts @@ -0,0 +1,8 @@ +import type { ClientsArray } from './clientsArray'; +import type { ClientsAutoArray } from './clientsAutoArray'; + +export interface Clients { + clients?: ClientsArray; + auto_clients?: ClientsAutoArray; + supported_tags?: string[]; +} diff --git a/client_v2/src/api/model/clientsArray.ts b/client_v2/src/api/model/clientsArray.ts new file mode 100644 index 000000000..b5c325c43 --- /dev/null +++ b/client_v2/src/api/model/clientsArray.ts @@ -0,0 +1,6 @@ +import type { Client } from './client'; + +/** + * Clients array + */ +export type ClientsArray = Client[]; diff --git a/client_v2/src/api/model/clientsAutoArray.ts b/client_v2/src/api/model/clientsAutoArray.ts new file mode 100644 index 000000000..8b4060700 --- /dev/null +++ b/client_v2/src/api/model/clientsAutoArray.ts @@ -0,0 +1,6 @@ +import type { ClientAuto } from './clientAuto'; + +/** + * Auto-Clients array + */ +export type ClientsAutoArray = ClientAuto[]; diff --git a/client_v2/src/api/model/clientsFindEntry.ts b/client_v2/src/api/model/clientsFindEntry.ts new file mode 100644 index 000000000..513556df2 --- /dev/null +++ b/client_v2/src/api/model/clientsFindEntry.ts @@ -0,0 +1,5 @@ +import type { ClientFindSubEntry } from './clientFindSubEntry'; + +export interface ClientsFindEntry { + [key: string]: ClientFindSubEntry; +} diff --git a/client_v2/src/api/model/clientsFindParams.ts b/client_v2/src/api/model/clientsFindParams.ts new file mode 100644 index 000000000..19bf5d5e7 --- /dev/null +++ b/client_v2/src/api/model/clientsFindParams.ts @@ -0,0 +1,7 @@ +export type ClientsFindParams = { + /** + * Filter by IP address or ClientIDs. Parameters with names `ip1`, `ip2`, and so on are also accepted and interpreted as "ip0 OR ip1 OR ip2". + * TODO(a.garipov): Replace with a better query API. + */ + ip0?: string; +}; diff --git a/client_v2/src/api/model/clientsFindResponse.ts b/client_v2/src/api/model/clientsFindResponse.ts new file mode 100644 index 000000000..6ff152d70 --- /dev/null +++ b/client_v2/src/api/model/clientsFindResponse.ts @@ -0,0 +1,6 @@ +import type { ClientsFindEntry } from './clientsFindEntry'; + +/** + * Client search results. + */ +export type ClientsFindResponse = ClientsFindEntry[]; diff --git a/client_v2/src/api/model/clientsSearchRequest.ts b/client_v2/src/api/model/clientsSearchRequest.ts new file mode 100644 index 000000000..4045e6de2 --- /dev/null +++ b/client_v2/src/api/model/clientsSearchRequest.ts @@ -0,0 +1,8 @@ +import type { ClientsSearchRequestItem } from './clientsSearchRequestItem'; + +/** + * Client search request + */ +export interface ClientsSearchRequest { + clients?: ClientsSearchRequestItem[]; +} diff --git a/client_v2/src/api/model/clientsSearchRequestItem.ts b/client_v2/src/api/model/clientsSearchRequestItem.ts new file mode 100644 index 000000000..138bf44a5 --- /dev/null +++ b/client_v2/src/api/model/clientsSearchRequestItem.ts @@ -0,0 +1,4 @@ +export interface ClientsSearchRequestItem { + /** Client IP address, CIDR, MAC address, or ClientID */ + id?: string; +} diff --git a/client_v2/src/api/model/dHCPNetInterface.ts b/client_v2/src/api/model/dHCPNetInterface.ts new file mode 100644 index 000000000..ebdffed8a --- /dev/null +++ b/client_v2/src/api/model/dHCPNetInterface.ts @@ -0,0 +1,15 @@ +/** + * DHCP network interface info + */ +export interface DHCPNetInterface { + /** Flags could be any combination of the following values, divided by the "|" character: "up", "broadcast", "loopback", "pointtopoint" and "multicast". */ + flags: string; + /** The IP address of the gateway. */ + gateway_ip: string; + hardware_address: string; + /** The addresses of the interface of v4 family. */ + ipv4_addresses: string[]; + /** The addresses of the interface of v6 family. */ + ipv6_addresses: string[]; + name: string; +} diff --git a/client_v2/src/api/model/dHCPNetInterfaces.ts b/client_v2/src/api/model/dHCPNetInterfaces.ts new file mode 100644 index 000000000..c8b6188b3 --- /dev/null +++ b/client_v2/src/api/model/dHCPNetInterfaces.ts @@ -0,0 +1,8 @@ +import type { DHCPNetInterface } from './dHCPNetInterface'; + +/** + * DHCP network interfaces dictionary, keys are interface names. + */ +export interface DHCPNetInterfaces { + [key: string]: DHCPNetInterface; +} diff --git a/client_v2/src/api/model/dNSConfig.ts b/client_v2/src/api/model/dNSConfig.ts new file mode 100644 index 000000000..dd998fdd4 --- /dev/null +++ b/client_v2/src/api/model/dNSConfig.ts @@ -0,0 +1,69 @@ +import type { DNSConfigBlockingMode } from './dNSConfigBlockingMode'; +import type { DNSConfigUpstreamMode } from './dNSConfigUpstreamMode'; + +/** + * DNS server configuration + */ +export interface DNSConfig { + /** Bootstrap servers, port is optional after colon. Empty value will reset it to default values. Comments are allowed, a comment line starts with a `#` symbol. */ + bootstrap_dns?: string[]; + /** Upstream servers, port is optional after colon. Empty value will reset it to default values. Comments are allowed, a comment line starts with a `#` symbol. */ + upstream_dns?: string[]; + /** List of fallback DNS servers used when upstream DNS servers are not responding. Empty value will clear the list. Comments are allowed, a comment line starts with a `#` symbol. */ + fallback_dns?: string[]; + upstream_dns_file?: string; + protection_enabled?: boolean; + ratelimit?: number; + /** + * Length of the subnet mask for IPv4 addresses. + * @minimum 0 + * @maximum 32 + */ + ratelimit_subnet_len_ipv4?: number; + /** + * Length of the subnet mask for IPv6 addresses. + * @minimum 0 + * @maximum 128 + */ + ratelimit_subnet_len_ipv6?: number; + /** List of IP addresses excluded from rate limiting. */ + ratelimit_whitelist?: string[]; + blocking_mode?: DNSConfigBlockingMode; + blocking_ipv4?: string; + blocking_ipv6?: string; + /** + * TTL for blocked responses. + * @minimum 0 + */ + blocked_response_ttl?: number; + /** Protection is pause until this time. Nullable. */ + protection_disabled_until?: string; + edns_cs_enabled?: boolean; + edns_cs_use_custom?: boolean; + edns_cs_custom_ip?: string; + disable_ipv6?: boolean; + dnssec_enabled?: boolean; + cache_size?: number; + cache_ttl_min?: number; + cache_ttl_max?: number; + /** + * Enables or disables the DNS response cache. + * + * If `cache_enabled` is `true`, the companion field `cache_size` must + * be present and greater than 0, or the `dns.cache_size` setting in + * the configuration file must already be greater than 0. + */ + cache_enabled?: boolean; + cache_optimistic?: boolean; + /** Upstream modes enumeration. The empty string value is deprecated; use `load_balance` instead. */ + upstream_mode?: DNSConfigUpstreamMode; + use_private_ptr_resolvers?: boolean; + resolve_clients?: boolean; + /** Upstream servers, port is optional after colon. Empty value will reset it to default values. Comments are allowed, a comment line starts with a `#` symbol. */ + local_ptr_upstreams?: string[]; + /** + * The number of seconds to wait for a response from the upstream server + * @minimum 1 + */ + upstream_timeout?: number; +} diff --git a/client_v2/src/api/model/dNSConfigBlockingMode.ts b/client_v2/src/api/model/dNSConfigBlockingMode.ts new file mode 100644 index 000000000..f321af641 --- /dev/null +++ b/client_v2/src/api/model/dNSConfigBlockingMode.ts @@ -0,0 +1 @@ +export type DNSConfigBlockingMode = 'default' | 'refused' | 'nxdomain' | 'null_ip' | 'custom_ip'; diff --git a/client_v2/src/api/model/dNSConfigUpstreamMode.ts b/client_v2/src/api/model/dNSConfigUpstreamMode.ts new file mode 100644 index 000000000..87695f7cd --- /dev/null +++ b/client_v2/src/api/model/dNSConfigUpstreamMode.ts @@ -0,0 +1,4 @@ +/** + * Upstream modes enumeration. The empty string value is deprecated; use `load_balance` instead. + */ +export type DNSConfigUpstreamMode = '' | 'fastest_addr' | 'load_balance' | 'parallel'; diff --git a/client_v2/src/api/model/dayRange.ts b/client_v2/src/api/model/dayRange.ts new file mode 100644 index 000000000..ef3021de3 --- /dev/null +++ b/client_v2/src/api/model/dayRange.ts @@ -0,0 +1,17 @@ +/** + * The single interval within a day. It begins at the `start` and ends before the `end`. + */ +export interface DayRange { + /** + * The number of milliseconds elapsed from the start of a day. It must be less than `end` and is expected to be rounded to minutes. So the maximum value is `86340000` (23 hours and 59 minutes). + * @minimum 0 + * @maximum 86340000 + */ + start?: number; + /** + * The number of milliseconds elapsed from the start of a day. It is expected to be rounded to minutes. The maximum value is `86400000` (24 hours). + * @minimum 0 + * @maximum 86400000 + */ + end?: number; +} diff --git a/client_v2/src/api/model/dhcpConfig.ts b/client_v2/src/api/model/dhcpConfig.ts new file mode 100644 index 000000000..a084e5d91 --- /dev/null +++ b/client_v2/src/api/model/dhcpConfig.ts @@ -0,0 +1,9 @@ +import type { DhcpConfigV4 } from './dhcpConfigV4'; +import type { DhcpConfigV6 } from './dhcpConfigV6'; + +export interface DhcpConfig { + enabled?: boolean; + interface_name?: string; + v4?: DhcpConfigV4; + v6?: DhcpConfigV6; +} diff --git a/client_v2/src/api/model/dhcpConfigV4.ts b/client_v2/src/api/model/dhcpConfigV4.ts new file mode 100644 index 000000000..6fbbab634 --- /dev/null +++ b/client_v2/src/api/model/dhcpConfigV4.ts @@ -0,0 +1,7 @@ +export interface DhcpConfigV4 { + gateway_ip: string; + subnet_mask: string; + range_start: string; + range_end: string; + lease_duration: number; +} diff --git a/client_v2/src/api/model/dhcpConfigV6.ts b/client_v2/src/api/model/dhcpConfigV6.ts new file mode 100644 index 000000000..9d03f1114 --- /dev/null +++ b/client_v2/src/api/model/dhcpConfigV6.ts @@ -0,0 +1,4 @@ +export interface DhcpConfigV6 { + range_start?: string; + lease_duration?: number; +} diff --git a/client_v2/src/api/model/dhcpFindActiveReq.ts b/client_v2/src/api/model/dhcpFindActiveReq.ts new file mode 100644 index 000000000..4b1ebf5a3 --- /dev/null +++ b/client_v2/src/api/model/dhcpFindActiveReq.ts @@ -0,0 +1,7 @@ +/** + * Request for checking for other DHCP servers in the network. + */ +export interface DhcpFindActiveReq { + /** The name of the network interface */ + interface?: string; +} diff --git a/client_v2/src/api/model/dhcpLease.ts b/client_v2/src/api/model/dhcpLease.ts new file mode 100644 index 000000000..15f0e3e3f --- /dev/null +++ b/client_v2/src/api/model/dhcpLease.ts @@ -0,0 +1,9 @@ +/** + * DHCP lease information + */ +export interface DhcpLease { + mac: string; + ip: string; + hostname: string; + expires: string; +} diff --git a/client_v2/src/api/model/dhcpSearchResult.ts b/client_v2/src/api/model/dhcpSearchResult.ts new file mode 100644 index 000000000..6aec1e85f --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchResult.ts @@ -0,0 +1,10 @@ +import type { DhcpSearchV4 } from './dhcpSearchV4'; +import type { DhcpSearchV6 } from './dhcpSearchV6'; + +/** + * Information about a DHCP server discovered in the current network. + */ +export interface DhcpSearchResult { + v4?: DhcpSearchV4; + v6?: DhcpSearchV6; +} diff --git a/client_v2/src/api/model/dhcpSearchResultOtherServer.ts b/client_v2/src/api/model/dhcpSearchResultOtherServer.ts new file mode 100644 index 000000000..2580ed3eb --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchResultOtherServer.ts @@ -0,0 +1,8 @@ +import type { DhcpSearchResultOtherServerFound } from './dhcpSearchResultOtherServerFound'; + +export interface DhcpSearchResultOtherServer { + /** The result of searching the other DHCP server. */ + found?: DhcpSearchResultOtherServerFound; + /** Set if found=error */ + error?: string; +} diff --git a/client_v2/src/api/model/dhcpSearchResultOtherServerFound.ts b/client_v2/src/api/model/dhcpSearchResultOtherServerFound.ts new file mode 100644 index 000000000..2051f79e6 --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchResultOtherServerFound.ts @@ -0,0 +1,4 @@ +/** + * The result of searching the other DHCP server. + */ +export type DhcpSearchResultOtherServerFound = 'yes' | 'no' | 'error'; diff --git a/client_v2/src/api/model/dhcpSearchResultStaticIP.ts b/client_v2/src/api/model/dhcpSearchResultStaticIP.ts new file mode 100644 index 000000000..900ed73f4 --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchResultStaticIP.ts @@ -0,0 +1,8 @@ +import type { DhcpSearchResultStaticIPStatic } from './dhcpSearchResultStaticIPStatic'; + +export interface DhcpSearchResultStaticIP { + /** The result of determining static IP address. */ + static?: DhcpSearchResultStaticIPStatic; + /** Set if static=no */ + ip?: string; +} diff --git a/client_v2/src/api/model/dhcpSearchResultStaticIPStatic.ts b/client_v2/src/api/model/dhcpSearchResultStaticIPStatic.ts new file mode 100644 index 000000000..a3d023e09 --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchResultStaticIPStatic.ts @@ -0,0 +1,4 @@ +/** + * The result of determining static IP address. + */ +export type DhcpSearchResultStaticIPStatic = 'yes' | 'no' | 'error'; diff --git a/client_v2/src/api/model/dhcpSearchV4.ts b/client_v2/src/api/model/dhcpSearchV4.ts new file mode 100644 index 000000000..400b2538e --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchV4.ts @@ -0,0 +1,7 @@ +import type { DhcpSearchResultOtherServer } from './dhcpSearchResultOtherServer'; +import type { DhcpSearchResultStaticIP } from './dhcpSearchResultStaticIP'; + +export interface DhcpSearchV4 { + other_server?: DhcpSearchResultOtherServer; + static_ip?: DhcpSearchResultStaticIP; +} diff --git a/client_v2/src/api/model/dhcpSearchV6.ts b/client_v2/src/api/model/dhcpSearchV6.ts new file mode 100644 index 000000000..02e583e3b --- /dev/null +++ b/client_v2/src/api/model/dhcpSearchV6.ts @@ -0,0 +1,5 @@ +import type { DhcpSearchResultOtherServer } from './dhcpSearchResultOtherServer'; + +export interface DhcpSearchV6 { + other_server?: DhcpSearchResultOtherServer; +} diff --git a/client_v2/src/api/model/dhcpStaticLease.ts b/client_v2/src/api/model/dhcpStaticLease.ts new file mode 100644 index 000000000..42ef96c44 --- /dev/null +++ b/client_v2/src/api/model/dhcpStaticLease.ts @@ -0,0 +1,8 @@ +/** + * DHCP static lease information + */ +export interface DhcpStaticLease { + mac: string; + ip: string; + hostname: string; +} diff --git a/client_v2/src/api/model/dhcpStaticLeaseBody.ts b/client_v2/src/api/model/dhcpStaticLeaseBody.ts new file mode 100644 index 000000000..efca7f8c3 --- /dev/null +++ b/client_v2/src/api/model/dhcpStaticLeaseBody.ts @@ -0,0 +1,3 @@ +import type { DhcpStaticLease } from './dhcpStaticLease'; + +export type DhcpStaticLeaseBody = DhcpStaticLease; diff --git a/client_v2/src/api/model/dhcpStatus.ts b/client_v2/src/api/model/dhcpStatus.ts new file mode 100644 index 000000000..b1c28dd29 --- /dev/null +++ b/client_v2/src/api/model/dhcpStatus.ts @@ -0,0 +1,16 @@ +import type { DhcpConfigV4 } from './dhcpConfigV4'; +import type { DhcpConfigV6 } from './dhcpConfigV6'; +import type { DhcpLease } from './dhcpLease'; +import type { DhcpStaticLease } from './dhcpStaticLease'; + +/** + * Built-in DHCP server configuration and status + */ +export interface DhcpStatus { + enabled?: boolean; + interface_name?: string; + v4?: DhcpConfigV4; + v6?: DhcpConfigV6; + leases: DhcpLease[]; + static_leases?: DhcpStaticLease[]; +} diff --git a/client_v2/src/api/model/dnsAnswer.ts b/client_v2/src/api/model/dnsAnswer.ts new file mode 100644 index 000000000..7a484ea3e --- /dev/null +++ b/client_v2/src/api/model/dnsAnswer.ts @@ -0,0 +1,8 @@ +/** + * DNS answer section + */ +export interface DnsAnswer { + ttl?: number; + type?: string; + value?: string; +} diff --git a/client_v2/src/api/model/dnsInfo200.ts b/client_v2/src/api/model/dnsInfo200.ts new file mode 100644 index 000000000..7004231bd --- /dev/null +++ b/client_v2/src/api/model/dnsInfo200.ts @@ -0,0 +1,5 @@ +import type { DNSConfig } from './dNSConfig'; + +export type DnsInfo200 = DNSConfig & { + default_local_ptr_upstreams?: string[]; +}; diff --git a/client_v2/src/api/model/dnsQuestion.ts b/client_v2/src/api/model/dnsQuestion.ts new file mode 100644 index 000000000..851452d45 --- /dev/null +++ b/client_v2/src/api/model/dnsQuestion.ts @@ -0,0 +1,9 @@ +/** + * DNS question section + */ +export interface DnsQuestion { + class?: string; + name?: string; + unicode_name?: string; + type?: string; +} diff --git a/client_v2/src/api/model/error.ts b/client_v2/src/api/model/error.ts new file mode 100644 index 000000000..5ac8990de --- /dev/null +++ b/client_v2/src/api/model/error.ts @@ -0,0 +1,7 @@ +/** + * A generic JSON error response. + */ +export interface Error { + /** The error message, an opaque string. */ + message?: string; +} diff --git a/client_v2/src/api/model/filter.ts b/client_v2/src/api/model/filter.ts new file mode 100644 index 000000000..b75d85af3 --- /dev/null +++ b/client_v2/src/api/model/filter.ts @@ -0,0 +1,11 @@ +/** + * Filter subscription info + */ +export interface Filter { + enabled: boolean; + id: number; + last_updated?: string; + name: string; + rules_count: number; + url: string; +} diff --git a/client_v2/src/api/model/filterCheckHostResponse.ts b/client_v2/src/api/model/filterCheckHostResponse.ts new file mode 100644 index 000000000..02db42609 --- /dev/null +++ b/client_v2/src/api/model/filterCheckHostResponse.ts @@ -0,0 +1,29 @@ +import type { FilteringReason } from './filteringReason'; +import type { ResultRule } from './resultRule'; + +/** + * Check Host Result + */ +export interface FilterCheckHostResponse { + reason?: FilteringReason; + /** + * In case if there's a rule applied to this DNS request, this is ID of the filter list that the rule belongs to. + * Deprecated: use `rules[*].filter_list_id` instead. + * @deprecated + */ + filter_id?: number; + /** + * Filtering rule applied to the request (if any). + * Deprecated: use `rules[*].text` instead. + * @deprecated + */ + rule?: string; + /** Applied rules. */ + rules?: ResultRule[]; + /** Set if reason=FilteredBlockedService */ + service_name?: string; + /** Set if reason=Rewrite */ + cname?: string; + /** Set if reason=Rewrite */ + ip_addrs?: string[]; +} diff --git a/client_v2/src/api/model/filterConfig.ts b/client_v2/src/api/model/filterConfig.ts new file mode 100644 index 000000000..315031229 --- /dev/null +++ b/client_v2/src/api/model/filterConfig.ts @@ -0,0 +1,7 @@ +/** + * Filtering settings + */ +export interface FilterConfig { + enabled?: boolean; + interval?: number; +} diff --git a/client_v2/src/api/model/filterRefreshRequest.ts b/client_v2/src/api/model/filterRefreshRequest.ts new file mode 100644 index 000000000..5d421c41a --- /dev/null +++ b/client_v2/src/api/model/filterRefreshRequest.ts @@ -0,0 +1,6 @@ +/** + * Refresh Filters request data + */ +export interface FilterRefreshRequest { + whitelist?: boolean; +} diff --git a/client_v2/src/api/model/filterRefreshResponse.ts b/client_v2/src/api/model/filterRefreshResponse.ts new file mode 100644 index 000000000..ff0105f42 --- /dev/null +++ b/client_v2/src/api/model/filterRefreshResponse.ts @@ -0,0 +1,6 @@ +/** + * /filtering/refresh response data + */ +export interface FilterRefreshResponse { + updated?: number; +} diff --git a/client_v2/src/api/model/filterSetUrl.ts b/client_v2/src/api/model/filterSetUrl.ts new file mode 100644 index 000000000..48c5abf2a --- /dev/null +++ b/client_v2/src/api/model/filterSetUrl.ts @@ -0,0 +1,10 @@ +import type { FilterSetUrlData } from './filterSetUrlData'; + +/** + * Filtering URL settings + */ +export interface FilterSetUrl { + data?: FilterSetUrlData; + url?: string; + whitelist?: boolean; +} diff --git a/client_v2/src/api/model/filterSetUrlData.ts b/client_v2/src/api/model/filterSetUrlData.ts new file mode 100644 index 000000000..1deefa8ce --- /dev/null +++ b/client_v2/src/api/model/filterSetUrlData.ts @@ -0,0 +1,8 @@ +/** + * Filter update data + */ +export interface FilterSetUrlData { + enabled: boolean; + name: string; + url: string; +} diff --git a/client_v2/src/api/model/filterStatus.ts b/client_v2/src/api/model/filterStatus.ts new file mode 100644 index 000000000..75b1bd863 --- /dev/null +++ b/client_v2/src/api/model/filterStatus.ts @@ -0,0 +1,12 @@ +import type { Filter } from './filter'; + +/** + * Filtering settings + */ +export interface FilterStatus { + enabled?: boolean; + interval?: number; + filters?: Filter[]; + whitelist_filters?: Filter[]; + user_rules?: string[]; +} diff --git a/client_v2/src/api/model/filteringCheckHostParams.ts b/client_v2/src/api/model/filteringCheckHostParams.ts new file mode 100644 index 000000000..4623a960c --- /dev/null +++ b/client_v2/src/api/model/filteringCheckHostParams.ts @@ -0,0 +1,14 @@ +export type FilteringCheckHostParams = { + /** + * Filter by host name + */ + name: string; + /** + * Optional ClientID or client IP address + */ + client?: string; + /** + * Optional DNS type + */ + qtype?: string; +}; diff --git a/client_v2/src/api/model/filteringReason.ts b/client_v2/src/api/model/filteringReason.ts new file mode 100644 index 000000000..96e5c4759 --- /dev/null +++ b/client_v2/src/api/model/filteringReason.ts @@ -0,0 +1,16 @@ +/** + * Request filtering status. + */ +export type FilteringReason = + | 'NotFilteredNotFound' + | 'NotFilteredWhiteList' + | 'NotFilteredError' + | 'FilteredBlackList' + | 'FilteredSafeBrowsing' + | 'FilteredParental' + | 'FilteredInvalid' + | 'FilteredSafeSearch' + | 'FilteredBlockedService' + | 'Rewrite' + | 'RewriteEtcHosts' + | 'RewriteRule'; diff --git a/client_v2/src/api/model/getQueryLogConfigResponse.ts b/client_v2/src/api/model/getQueryLogConfigResponse.ts new file mode 100644 index 000000000..097bda8ea --- /dev/null +++ b/client_v2/src/api/model/getQueryLogConfigResponse.ts @@ -0,0 +1,15 @@ +/** + * Query log configuration + */ +export interface GetQueryLogConfigResponse { + /** Is query log enabled */ + enabled: boolean; + /** Time period for query log rotation in milliseconds. */ + interval: number; + /** Anonymize clients' IP addresses */ + anonymize_client_ip: boolean; + /** List of host names, which should not be written to log */ + ignored: string[]; + /** If true, the host names in the `ignored` array are excluded from the query log. */ + ignored_enabled?: boolean; +} diff --git a/client_v2/src/api/model/getStatsConfigResponse.ts b/client_v2/src/api/model/getStatsConfigResponse.ts new file mode 100644 index 000000000..852a87cef --- /dev/null +++ b/client_v2/src/api/model/getStatsConfigResponse.ts @@ -0,0 +1,13 @@ +/** + * Statistics configuration + */ +export interface GetStatsConfigResponse { + /** Are statistics enabled */ + enabled: boolean; + /** Statistics rotation interval in milliseconds */ + interval: number; + /** List of host names, which should not be counted */ + ignored: string[]; + /** If true, the host names in the `ignored` array are excluded from the statistics. */ + ignored_enabled?: boolean; +} diff --git a/client_v2/src/api/model/getVersionRequest.ts b/client_v2/src/api/model/getVersionRequest.ts new file mode 100644 index 000000000..8e738da5a --- /dev/null +++ b/client_v2/src/api/model/getVersionRequest.ts @@ -0,0 +1,7 @@ +/** + * /version.json request data + */ +export interface GetVersionRequest { + /** If false, server will check for a new version data only once in several hours. */ + recheck_now?: boolean; +} diff --git a/client_v2/src/api/model/index.ts b/client_v2/src/api/model/index.ts new file mode 100644 index 000000000..9a278d81b --- /dev/null +++ b/client_v2/src/api/model/index.ts @@ -0,0 +1,118 @@ +export * from './accessList'; +export * from './accessListResponse'; +export * from './accessSetRequest'; +export * from './addressesInfo'; +export * from './addressInfo'; +export * from './addUrlRequest'; +export * from './blockedService'; +export * from './blockedServicesAll'; +export * from './blockedServicesArray'; +export * from './blockedServicesSchedule'; +export * from './checkConfigRequest'; +export * from './checkConfigRequestInfo'; +export * from './checkConfigResponse'; +export * from './checkConfigResponseInfo'; +export * from './checkConfigStaticIpInfo'; +export * from './checkConfigStaticIpInfoStatic'; +export * from './client'; +export * from './clientAuto'; +export * from './clientDelete'; +export * from './clientFindSubEntry'; +export * from './clients'; +export * from './clientsArray'; +export * from './clientsAutoArray'; +export * from './clientsFindEntry'; +export * from './clientsFindParams'; +export * from './clientsFindResponse'; +export * from './clientsSearchRequest'; +export * from './clientsSearchRequestItem'; +export * from './clientUpdate'; +export * from './dayRange'; +export * from './dhcpConfig'; +export * from './dhcpConfigV4'; +export * from './dhcpConfigV6'; +export * from './dhcpFindActiveReq'; +export * from './dhcpLease'; +export * from './dHCPNetInterface'; +export * from './dHCPNetInterfaces'; +export * from './dhcpSearchResult'; +export * from './dhcpSearchResultOtherServer'; +export * from './dhcpSearchResultOtherServerFound'; +export * from './dhcpSearchResultStaticIP'; +export * from './dhcpSearchResultStaticIPStatic'; +export * from './dhcpSearchV4'; +export * from './dhcpSearchV6'; +export * from './dhcpStaticLease'; +export * from './dhcpStaticLeaseBody'; +export * from './dhcpStatus'; +export * from './dnsAnswer'; +export * from './dNSConfig'; +export * from './dNSConfigBlockingMode'; +export * from './dNSConfigUpstreamMode'; +export * from './dnsInfo200'; +export * from './dnsQuestion'; +export * from './error'; +export * from './filter'; +export * from './filterCheckHostResponse'; +export * from './filterConfig'; +export * from './filteringCheckHostParams'; +export * from './filteringReason'; +export * from './filterRefreshRequest'; +export * from './filterRefreshResponse'; +export * from './filterSetUrl'; +export * from './filterSetUrlData'; +export * from './filterStatus'; +export * from './getQueryLogConfigResponse'; +export * from './getStatsConfigResponse'; +export * from './getVersionRequest'; +export * from './initialConfiguration'; +export * from './lang'; +export * from './languageSettings'; +export * from './login'; +export * from './mobileConfigDoHParams'; +export * from './mobileConfigDoTParams'; +export * from './netInterface'; +export * from './netInterfaces'; +export * from './parentalStatus200'; +export * from './profileInfo'; +export * from './profileInfoTheme'; +export * from './putQueryLogConfigUpdateRequest'; +export * from './putStatsConfigUpdateRequest'; +export * from './queryLog'; +export * from './queryLogConfig'; +export * from './queryLogConfigInterval'; +export * from './queryLogItem'; +export * from './queryLogItemClient'; +export * from './queryLogItemClientProto'; +export * from './queryLogItemClientWhois'; +export * from './queryLogParams'; +export * from './queryLogResponseStatus'; +export * from './removeUrlRequest'; +export * from './resultRule'; +export * from './rewriteEntry'; +export * from './rewriteEntryBody'; +export * from './rewriteList'; +export * from './rewriteSettings'; +export * from './rewriteSettingsBody'; +export * from './rewriteUpdate'; +export * from './rewriteUpdateBody'; +export * from './safebrowsingStatus200'; +export * from './safeSearchConfig'; +export * from './schedule'; +export * from './serverStatus'; +export * from './serviceGroup'; +export * from './setProtectionRequest'; +export * from './setRulesRequest'; +export * from './stats'; +export * from './statsConfig'; +export * from './statsConfigInterval'; +export * from './statsParams'; +export * from './statsTimeUnits'; +export * from './tlsConfig'; +export * from './tlsConfigBody'; +export * from './tlsConfigKeyType'; +export * from './topArrayEntry'; +export * from './upstreamsConfig'; +export * from './upstreamsConfigResponse'; +export * from './versionInfo'; +export * from './whoisInfo'; diff --git a/client_v2/src/api/model/initialConfiguration.ts b/client_v2/src/api/model/initialConfiguration.ts new file mode 100644 index 000000000..ba4c9dc42 --- /dev/null +++ b/client_v2/src/api/model/initialConfiguration.ts @@ -0,0 +1,15 @@ +import type { AddressInfo } from './addressInfo'; +import type { Lang } from './lang'; + +/** + * AdGuard Home initial configuration for the first-install wizard. + */ +export interface InitialConfiguration { + dns: AddressInfo; + web: AddressInfo; + language?: Lang; + /** Basic auth password */ + password: string; + /** Basic auth username */ + username: string; +} diff --git a/client_v2/src/api/model/lang.ts b/client_v2/src/api/model/lang.ts new file mode 100644 index 000000000..3caeb379b --- /dev/null +++ b/client_v2/src/api/model/lang.ts @@ -0,0 +1,40 @@ +/** + * Language code. + */ +export type Lang = + | 'ar' + | 'be' + | 'bg' + | 'cs' + | 'da' + | 'de' + | 'en' + | 'es' + | 'fa' + | 'fi' + | 'fr' + | 'hr' + | 'hu' + | 'id' + | 'it' + | 'ja' + | 'ko' + | 'nl' + | 'no' + | 'pl' + | 'pt-br' + | 'pt-pt' + | 'ro' + | 'ru' + | 'si-lk' + | 'sk' + | 'sl' + | 'sr-cs' + | 'sv' + | 'th' + | 'tr' + | 'uk' + | 'vi' + | 'zh-cn' + | 'zh-hk' + | 'zh-tw'; diff --git a/client_v2/src/api/model/languageSettings.ts b/client_v2/src/api/model/languageSettings.ts new file mode 100644 index 000000000..189185027 --- /dev/null +++ b/client_v2/src/api/model/languageSettings.ts @@ -0,0 +1,8 @@ +import type { Lang } from './lang'; + +/** + * Language settings object. + */ +export interface LanguageSettings { + language: Lang; +} diff --git a/client_v2/src/api/model/login.ts b/client_v2/src/api/model/login.ts new file mode 100644 index 000000000..8cf73c19c --- /dev/null +++ b/client_v2/src/api/model/login.ts @@ -0,0 +1,9 @@ +/** + * Login request data + */ +export interface Login { + /** User name */ + name?: string; + /** Password */ + password?: string; +} diff --git a/client_v2/src/api/model/mobileConfigDoHParams.ts b/client_v2/src/api/model/mobileConfigDoHParams.ts new file mode 100644 index 000000000..9b6b1284c --- /dev/null +++ b/client_v2/src/api/model/mobileConfigDoHParams.ts @@ -0,0 +1,10 @@ +export type MobileConfigDoHParams = { + /** + * Host for which the config is generated. If no host is provided, `tls.server_name` from the configuration file is used. If `tls.server_name` is not set, the API returns an error with a 500 status. + */ + host: string; + /** + * ClientID. + */ + client_id?: string; +}; diff --git a/client_v2/src/api/model/mobileConfigDoTParams.ts b/client_v2/src/api/model/mobileConfigDoTParams.ts new file mode 100644 index 000000000..b788d41e1 --- /dev/null +++ b/client_v2/src/api/model/mobileConfigDoTParams.ts @@ -0,0 +1,10 @@ +export type MobileConfigDoTParams = { + /** + * Host for which the config is generated. If no host is provided, `tls.server_name` from the configuration file is used. If `tls.server_name` is not set, the API returns an error with a 500 status. + */ + host: string; + /** + * ClientID. + */ + client_id?: string; +}; diff --git a/client_v2/src/api/model/netInterface.ts b/client_v2/src/api/model/netInterface.ts new file mode 100644 index 000000000..3ac55b4b9 --- /dev/null +++ b/client_v2/src/api/model/netInterface.ts @@ -0,0 +1,13 @@ +/** + * Network interface info + */ +export interface NetInterface { + /** Flags could be any combination of the following values, divided by the "|" character: "up", "broadcast", "loopback", "pointtopoint" and "multicast". */ + flags: string; + hardware_address: string; + /** The addresses of the interface. */ + ip_addresses: string[]; + /** MTU value of the interface. */ + mtu: number; + name: string; +} diff --git a/client_v2/src/api/model/netInterfaces.ts b/client_v2/src/api/model/netInterfaces.ts new file mode 100644 index 000000000..c1b3a656c --- /dev/null +++ b/client_v2/src/api/model/netInterfaces.ts @@ -0,0 +1,8 @@ +import type { NetInterface } from './netInterface'; + +/** + * Network interfaces dictionary, keys are interface names. + */ +export interface NetInterfaces { + [key: string]: NetInterface; +} diff --git a/client_v2/src/api/model/parentalStatus200.ts b/client_v2/src/api/model/parentalStatus200.ts new file mode 100644 index 000000000..465fbc013 --- /dev/null +++ b/client_v2/src/api/model/parentalStatus200.ts @@ -0,0 +1,4 @@ +export type ParentalStatus200 = { + enabled?: boolean; + sensitivity?: number; +}; diff --git a/client_v2/src/api/model/profileInfo.ts b/client_v2/src/api/model/profileInfo.ts new file mode 100644 index 000000000..630e4462c --- /dev/null +++ b/client_v2/src/api/model/profileInfo.ts @@ -0,0 +1,12 @@ +import type { Lang } from './lang'; +import type { ProfileInfoTheme } from './profileInfoTheme'; + +/** + * Information about the current user + */ +export interface ProfileInfo { + name: string; + language: Lang; + /** Interface theme */ + theme: ProfileInfoTheme; +} diff --git a/client_v2/src/api/model/profileInfoTheme.ts b/client_v2/src/api/model/profileInfoTheme.ts new file mode 100644 index 000000000..89062d5ff --- /dev/null +++ b/client_v2/src/api/model/profileInfoTheme.ts @@ -0,0 +1,4 @@ +/** + * Interface theme + */ +export type ProfileInfoTheme = 'auto' | 'dark' | 'light'; diff --git a/client_v2/src/api/model/putQueryLogConfigUpdateRequest.ts b/client_v2/src/api/model/putQueryLogConfigUpdateRequest.ts new file mode 100644 index 000000000..cd2cd46b7 --- /dev/null +++ b/client_v2/src/api/model/putQueryLogConfigUpdateRequest.ts @@ -0,0 +1,3 @@ +import type { GetQueryLogConfigResponse } from './getQueryLogConfigResponse'; + +export type PutQueryLogConfigUpdateRequest = GetQueryLogConfigResponse; diff --git a/client_v2/src/api/model/putStatsConfigUpdateRequest.ts b/client_v2/src/api/model/putStatsConfigUpdateRequest.ts new file mode 100644 index 000000000..ecc072790 --- /dev/null +++ b/client_v2/src/api/model/putStatsConfigUpdateRequest.ts @@ -0,0 +1,3 @@ +import type { GetStatsConfigResponse } from './getStatsConfigResponse'; + +export type PutStatsConfigUpdateRequest = GetStatsConfigResponse; diff --git a/client_v2/src/api/model/queryLog.ts b/client_v2/src/api/model/queryLog.ts new file mode 100644 index 000000000..8251ff385 --- /dev/null +++ b/client_v2/src/api/model/queryLog.ts @@ -0,0 +1,9 @@ +import type { QueryLogItem } from './queryLogItem'; + +/** + * Query log + */ +export interface QueryLog { + oldest?: string; + data?: QueryLogItem[]; +} diff --git a/client_v2/src/api/model/queryLogConfig.ts b/client_v2/src/api/model/queryLogConfig.ts new file mode 100644 index 000000000..39e15a182 --- /dev/null +++ b/client_v2/src/api/model/queryLogConfig.ts @@ -0,0 +1,13 @@ +import type { QueryLogConfigInterval } from './queryLogConfigInterval'; + +/** + * Query log configuration + */ +export interface QueryLogConfig { + /** Is query log enabled */ + enabled?: boolean; + /** Time period for query log rotation. */ + interval?: QueryLogConfigInterval; + /** Anonymize clients' IP addresses */ + anonymize_client_ip?: boolean; +} diff --git a/client_v2/src/api/model/queryLogConfigInterval.ts b/client_v2/src/api/model/queryLogConfigInterval.ts new file mode 100644 index 000000000..3dc2a8b66 --- /dev/null +++ b/client_v2/src/api/model/queryLogConfigInterval.ts @@ -0,0 +1,4 @@ +/** + * Time period for query log rotation. + */ +export type QueryLogConfigInterval = 0.25 | 1 | 7 | 30 | 90; diff --git a/client_v2/src/api/model/queryLogItem.ts b/client_v2/src/api/model/queryLogItem.ts new file mode 100644 index 000000000..5b74f4d6d --- /dev/null +++ b/client_v2/src/api/model/queryLogItem.ts @@ -0,0 +1,52 @@ +import type { DnsAnswer } from './dnsAnswer'; +import type { DnsQuestion } from './dnsQuestion'; +import type { FilteringReason } from './filteringReason'; +import type { QueryLogItemClient } from './queryLogItemClient'; +import type { QueryLogItemClientProto } from './queryLogItemClientProto'; +import type { ResultRule } from './resultRule'; + +/** + * Query log item + */ +export interface QueryLogItem { + answer?: DnsAnswer[]; + /** Answer from upstream server (optional) */ + original_answer?: DnsAnswer[]; + /** Defines if the response has been served from cache. */ + cached?: boolean; + /** Upstream URL starting with tcp://, tls://, https://, or with an IP address. */ + upstream?: string; + /** If true, the response had the Authenticated Data (AD) flag set. */ + answer_dnssec?: boolean; + /** The client's IP address. */ + client?: string; + /** The ClientID, if provided in DoH, DoQ, or DoT. */ + client_id?: string; + client_info?: QueryLogItemClient; + client_proto?: QueryLogItemClientProto; + /** The IP network defined by an EDNS Client-Subnet option in the request message if any. */ + ecs?: string; + elapsedMs?: string; + question?: DnsQuestion; + /** + * In case if there's a rule applied to this DNS request, this is ID of the filter list that the rule belongs to. + * Deprecated: use `rules[*].filter_list_id` instead. + * @deprecated + */ + filterId?: number; + /** + * Filtering rule applied to the request (if any). + * Deprecated: use `rules[*].text` instead. + * @deprecated + */ + rule?: string; + /** Applied rules. */ + rules?: ResultRule[]; + reason?: FilteringReason; + /** Set if reason=FilteredBlockedService */ + service_name?: string; + /** DNS response status */ + status?: string; + /** DNS request processing start time */ + time?: string; +} diff --git a/client_v2/src/api/model/queryLogItemClient.ts b/client_v2/src/api/model/queryLogItemClient.ts new file mode 100644 index 000000000..09b2e0d50 --- /dev/null +++ b/client_v2/src/api/model/queryLogItemClient.ts @@ -0,0 +1,14 @@ +import type { QueryLogItemClientWhois } from './queryLogItemClientWhois'; + +/** + * Client information for a query log item. + */ +export interface QueryLogItemClient { + /** Whether the client's IP is blocked or not. */ + disallowed: boolean; + /** The rule due to which the client is allowed or blocked. */ + disallowed_rule: string; + /** Persistent client's name or runtime client's hostname. May be empty. */ + name: string; + whois: QueryLogItemClientWhois; +} diff --git a/client_v2/src/api/model/queryLogItemClientProto.ts b/client_v2/src/api/model/queryLogItemClientProto.ts new file mode 100644 index 000000000..6efe5ad66 --- /dev/null +++ b/client_v2/src/api/model/queryLogItemClientProto.ts @@ -0,0 +1 @@ +export type QueryLogItemClientProto = 'dot' | 'doh' | 'doq' | 'dnscrypt' | ''; diff --git a/client_v2/src/api/model/queryLogItemClientWhois.ts b/client_v2/src/api/model/queryLogItemClientWhois.ts new file mode 100644 index 000000000..e65db419c --- /dev/null +++ b/client_v2/src/api/model/queryLogItemClientWhois.ts @@ -0,0 +1,11 @@ +/** + * Client WHOIS information, if any. + */ +export interface QueryLogItemClientWhois { + /** City, if any. */ + city?: string; + /** Country, if any. */ + country?: string; + /** Organization name, if any. */ + orgname?: string; +} diff --git a/client_v2/src/api/model/queryLogParams.ts b/client_v2/src/api/model/queryLogParams.ts new file mode 100644 index 000000000..301b44f87 --- /dev/null +++ b/client_v2/src/api/model/queryLogParams.ts @@ -0,0 +1,31 @@ +import type { FilteringReason } from './filteringReason'; +import type { QueryLogResponseStatus } from './queryLogResponseStatus'; + +export type QueryLogParams = { + /** + * Filter by older than + */ + older_than?: string; + /** + * Specify the ranking number of the first item on the page. Even though it is possible to use "offset" and "older_than", we recommend choosing one of them and sticking to it. + */ + offset?: number; + /** + * Limit the number of records to be returned + */ + limit?: number; + /** + * Filter by domain name or client IP + */ + search?: string; + /** + * Deprecated: Use 'reason' parameter instead Filter by response status + * NOTE: This parameter cannot be used with 'reason' parameter. + */ + response_status?: QueryLogResponseStatus; + /** + * Filter by response filtering reason. Multiple reasons can be provided. + * NOTE: This parameter cannot be used with 'response_status' parameter. + */ + reason?: FilteringReason[]; +}; diff --git a/client_v2/src/api/model/queryLogResponseStatus.ts b/client_v2/src/api/model/queryLogResponseStatus.ts new file mode 100644 index 000000000..86f72dd54 --- /dev/null +++ b/client_v2/src/api/model/queryLogResponseStatus.ts @@ -0,0 +1,10 @@ +export type QueryLogResponseStatus = + | 'all' + | 'filtered' + | 'blocked' + | 'blocked_safebrowsing' + | 'blocked_parental' + | 'whitelisted' + | 'rewritten' + | 'safe_search' + | 'processed'; diff --git a/client_v2/src/api/model/removeUrlRequest.ts b/client_v2/src/api/model/removeUrlRequest.ts new file mode 100644 index 000000000..8b4c2b6fa --- /dev/null +++ b/client_v2/src/api/model/removeUrlRequest.ts @@ -0,0 +1,8 @@ +/** + * /remove_url request data + */ +export interface RemoveUrlRequest { + /** Previously added URL containing filtering rules */ + url?: string; + whitelist?: boolean; +} diff --git a/client_v2/src/api/model/resultRule.ts b/client_v2/src/api/model/resultRule.ts new file mode 100644 index 000000000..6639e4a06 --- /dev/null +++ b/client_v2/src/api/model/resultRule.ts @@ -0,0 +1,9 @@ +/** + * Applied rule. + */ +export interface ResultRule { + /** In case if there's a rule applied to this DNS request, this is ID of the filter list that the rule belongs to. */ + filter_list_id?: number; + /** The text of the filtering rule applied to the request (if any). */ + text?: string; +} diff --git a/client_v2/src/api/model/rewriteEntry.ts b/client_v2/src/api/model/rewriteEntry.ts new file mode 100644 index 000000000..3e43830ae --- /dev/null +++ b/client_v2/src/api/model/rewriteEntry.ts @@ -0,0 +1,11 @@ +/** + * Rewrite rule + */ +export interface RewriteEntry { + /** Domain name */ + domain?: string; + /** value of A, AAAA or CNAME DNS record */ + answer?: string; + /** Optional. If omitted on add, defaults to `true`. On update, omitted preserves previous value. */ + enabled?: boolean; +} diff --git a/client_v2/src/api/model/rewriteEntryBody.ts b/client_v2/src/api/model/rewriteEntryBody.ts new file mode 100644 index 000000000..ea2c00d53 --- /dev/null +++ b/client_v2/src/api/model/rewriteEntryBody.ts @@ -0,0 +1,3 @@ +import type { RewriteEntry } from './rewriteEntry'; + +export type RewriteEntryBody = RewriteEntry; diff --git a/client_v2/src/api/model/rewriteList.ts b/client_v2/src/api/model/rewriteList.ts new file mode 100644 index 000000000..8f100b6d4 --- /dev/null +++ b/client_v2/src/api/model/rewriteList.ts @@ -0,0 +1,6 @@ +import type { RewriteEntry } from './rewriteEntry'; + +/** + * Rewrite rules array + */ +export type RewriteList = RewriteEntry[]; diff --git a/client_v2/src/api/model/rewriteSettings.ts b/client_v2/src/api/model/rewriteSettings.ts new file mode 100644 index 000000000..6b853b5c2 --- /dev/null +++ b/client_v2/src/api/model/rewriteSettings.ts @@ -0,0 +1,7 @@ +/** + * DNS rewrite settings + */ +export interface RewriteSettings { + /** indicates whether rewrites are applied */ + enabled: boolean; +} diff --git a/client_v2/src/api/model/rewriteSettingsBody.ts b/client_v2/src/api/model/rewriteSettingsBody.ts new file mode 100644 index 000000000..3ca3ea451 --- /dev/null +++ b/client_v2/src/api/model/rewriteSettingsBody.ts @@ -0,0 +1,3 @@ +import type { RewriteSettings } from './rewriteSettings'; + +export type RewriteSettingsBody = RewriteSettings; diff --git a/client_v2/src/api/model/rewriteUpdate.ts b/client_v2/src/api/model/rewriteUpdate.ts new file mode 100644 index 000000000..d0badddbd --- /dev/null +++ b/client_v2/src/api/model/rewriteUpdate.ts @@ -0,0 +1,9 @@ +import type { RewriteEntry } from './rewriteEntry'; + +/** + * Rewrite rule update object + */ +export interface RewriteUpdate { + target?: RewriteEntry; + update?: RewriteEntry; +} diff --git a/client_v2/src/api/model/rewriteUpdateBody.ts b/client_v2/src/api/model/rewriteUpdateBody.ts new file mode 100644 index 000000000..60a66d094 --- /dev/null +++ b/client_v2/src/api/model/rewriteUpdateBody.ts @@ -0,0 +1,3 @@ +import type { RewriteUpdate } from './rewriteUpdate'; + +export type RewriteUpdateBody = RewriteUpdate; diff --git a/client_v2/src/api/model/safeSearchConfig.ts b/client_v2/src/api/model/safeSearchConfig.ts new file mode 100644 index 000000000..a39221b22 --- /dev/null +++ b/client_v2/src/api/model/safeSearchConfig.ts @@ -0,0 +1,13 @@ +/** + * Safe search settings. + */ +export interface SafeSearchConfig { + enabled?: boolean; + bing?: boolean; + duckduckgo?: boolean; + ecosia?: boolean; + google?: boolean; + pixabay?: boolean; + yandex?: boolean; + youtube?: boolean; +} diff --git a/client_v2/src/api/model/safebrowsingStatus200.ts b/client_v2/src/api/model/safebrowsingStatus200.ts new file mode 100644 index 000000000..d02191cce --- /dev/null +++ b/client_v2/src/api/model/safebrowsingStatus200.ts @@ -0,0 +1,3 @@ +export type SafebrowsingStatus200 = { + enabled?: boolean; +}; diff --git a/client_v2/src/api/model/schedule.ts b/client_v2/src/api/model/schedule.ts new file mode 100644 index 000000000..81d374e12 --- /dev/null +++ b/client_v2/src/api/model/schedule.ts @@ -0,0 +1,16 @@ +import type { DayRange } from './dayRange'; + +/** + * Sets periods of inactivity for filtering blocked services. The schedule contains 7 days (Sunday to Saturday) and a time zone. + */ +export interface Schedule { + /** Time zone name according to IANA time zone database. For example `Europe/Brussels`. `Local` represents the system's local time zone. */ + time_zone?: string; + sun?: DayRange; + mon?: DayRange; + tue?: DayRange; + wed?: DayRange; + thu?: DayRange; + fri?: DayRange; + sat?: DayRange; +} diff --git a/client_v2/src/api/model/serverStatus.ts b/client_v2/src/api/model/serverStatus.ts new file mode 100644 index 000000000..f869e0937 --- /dev/null +++ b/client_v2/src/api/model/serverStatus.ts @@ -0,0 +1,26 @@ +import type { Lang } from './lang'; + +/** + * AdGuard Home server status and configuration + */ +export interface ServerStatus { + dns_addresses: string[]; + /** + * @minimum 1 + * @maximum 65535 + */ + dns_port: number; + /** + * @minimum 1 + * @maximum 65535 + */ + http_port: number; + protection_enabled: boolean; + protection_disabled_duration: number; + dhcp_available?: boolean; + running: boolean; + version: string; + language: Lang; + /** Start time of the web API server (Unix time in milliseconds). */ + start_time?: number; +} diff --git a/client_v2/src/api/model/serviceGroup.ts b/client_v2/src/api/model/serviceGroup.ts new file mode 100644 index 000000000..81772ea34 --- /dev/null +++ b/client_v2/src/api/model/serviceGroup.ts @@ -0,0 +1,4 @@ +export interface ServiceGroup { + /** The ID of this group. */ + id: string; +} diff --git a/client_v2/src/api/model/setProtectionRequest.ts b/client_v2/src/api/model/setProtectionRequest.ts new file mode 100644 index 000000000..faef74011 --- /dev/null +++ b/client_v2/src/api/model/setProtectionRequest.ts @@ -0,0 +1,8 @@ +/** + * Protection state configuration + */ +export interface SetProtectionRequest { + enabled: boolean; + /** Duration of a pause, in milliseconds. Enabled should be false. */ + duration?: number; +} diff --git a/client_v2/src/api/model/setRulesRequest.ts b/client_v2/src/api/model/setRulesRequest.ts new file mode 100644 index 000000000..67ae9e86b --- /dev/null +++ b/client_v2/src/api/model/setRulesRequest.ts @@ -0,0 +1,6 @@ +/** + * Custom filtering rules setting request. + */ +export interface SetRulesRequest { + rules?: string[]; +} diff --git a/client_v2/src/api/model/stats.ts b/client_v2/src/api/model/stats.ts new file mode 100644 index 000000000..84379e77b --- /dev/null +++ b/client_v2/src/api/model/stats.ts @@ -0,0 +1,39 @@ +import type { StatsTimeUnits } from './statsTimeUnits'; +import type { TopArrayEntry } from './topArrayEntry'; + +/** + * Server statistics data + */ +export interface Stats { + /** Time units */ + time_units?: StatsTimeUnits; + /** Total number of DNS queries */ + num_dns_queries?: number; + /** Number of requests blocked by filtering rules */ + num_blocked_filtering?: number; + /** Number of requests blocked by safebrowsing module */ + num_replaced_safebrowsing?: number; + /** Number of requests blocked by safesearch module */ + num_replaced_safesearch?: number; + /** Number of blocked adult websites */ + num_replaced_parental?: number; + /** Average time in seconds on processing a DNS request */ + avg_processing_time?: number; + top_queried_domains?: TopArrayEntry[]; + top_clients?: TopArrayEntry[]; + top_blocked_domains?: TopArrayEntry[]; + /** + * Total number of responses from each upstream. + * @maxItems 100 + */ + top_upstreams_responses?: TopArrayEntry[]; + /** + * Average processing time in seconds of requests from each upstream. + * @maxItems 100 + */ + top_upstreams_avg_time?: TopArrayEntry[]; + dns_queries?: number[]; + blocked_filtering?: number[]; + replaced_safebrowsing?: number[]; + replaced_parental?: number[]; +} diff --git a/client_v2/src/api/model/statsConfig.ts b/client_v2/src/api/model/statsConfig.ts new file mode 100644 index 000000000..a4025b351 --- /dev/null +++ b/client_v2/src/api/model/statsConfig.ts @@ -0,0 +1,9 @@ +import type { StatsConfigInterval } from './statsConfigInterval'; + +/** + * Statistics configuration + */ +export interface StatsConfig { + /** Time period to keep the data. `0` means that the statistics is disabled. */ + interval?: StatsConfigInterval; +} diff --git a/client_v2/src/api/model/statsConfigInterval.ts b/client_v2/src/api/model/statsConfigInterval.ts new file mode 100644 index 000000000..1dc8c4e14 --- /dev/null +++ b/client_v2/src/api/model/statsConfigInterval.ts @@ -0,0 +1,4 @@ +/** + * Time period to keep the data. `0` means that the statistics is disabled. + */ +export type StatsConfigInterval = 0 | 1 | 7 | 30 | 90; diff --git a/client_v2/src/api/model/statsParams.ts b/client_v2/src/api/model/statsParams.ts new file mode 100644 index 000000000..7bbb6fec6 --- /dev/null +++ b/client_v2/src/api/model/statsParams.ts @@ -0,0 +1,8 @@ +export type StatsParams = { + /** + * The lookback period for statistics in milliseconds. The interval must + * be a multiple of one hour and must not be greater than the value of + * `statistics.interval`. + */ + recent?: number; +}; diff --git a/client_v2/src/api/model/statsTimeUnits.ts b/client_v2/src/api/model/statsTimeUnits.ts new file mode 100644 index 000000000..cef2b7211 --- /dev/null +++ b/client_v2/src/api/model/statsTimeUnits.ts @@ -0,0 +1,4 @@ +/** + * Time units + */ +export type StatsTimeUnits = 'hours' | 'days'; diff --git a/client_v2/src/api/model/tlsConfig.ts b/client_v2/src/api/model/tlsConfig.ts new file mode 100644 index 000000000..3bd7add02 --- /dev/null +++ b/client_v2/src/api/model/tlsConfig.ts @@ -0,0 +1,57 @@ +import type { TlsConfigKeyType } from './tlsConfigKeyType'; + +/** + * TLS configuration settings and status + */ +export interface TlsConfig { + /** enabled is the encryption (DoT/DoH/HTTPS) status */ + enabled?: boolean; + /** server_name is the hostname of your HTTPS/TLS server */ + server_name?: string; + /** if true, forces HTTP->HTTPS redirect */ + force_https?: boolean; + /** HTTPS port. If 0, HTTPS will be disabled. */ + port_https?: number; + /** DNS-over-TLS port. If 0, DoT will be disabled. */ + port_dns_over_tls?: number; + /** DNS-over-QUIC port. If 0, DoQ will be disabled. */ + port_dns_over_quic?: number; + /** Base64 string with PEM-encoded certificates chain */ + certificate_chain?: string; + /** Base64 string with PEM-encoded private key */ + private_key?: string; + /** Set to true if the user has previously saved a private key as a string. This is used so that the server and the client don't have to send the private key between each other every time, which might lead to security issues. */ + private_key_saved?: boolean; + /** Path to certificate file */ + certificate_path?: string; + /** Path to private key file */ + private_key_path?: string; + /** Set to true if the specified certificates chain is a valid chain of X509 certificates. */ + valid_cert?: boolean; + /** Set to true if the specified certificates chain is verified and issued by a known CA. */ + valid_chain?: boolean; + /** The subject of the first certificate in the chain. */ + subject?: string; + /** The issuer of the first certificate in the chain. */ + issuer?: string; + /** The NotBefore field of the first certificate in the chain. */ + not_before?: string; + /** The NotAfter field of the first certificate in the chain. */ + not_after?: string; + /** The value of SubjectAltNames field of the first certificate in the chain. */ + dns_names?: string[]; + /** Set to true if the key is a valid private key. */ + valid_key?: boolean; + /** Key type. */ + key_type?: TlsConfigKeyType; + /** A validation warning message with the issue description. */ + warning_validation?: string; + /** Set to true if both certificate and private key are correct. */ + valid_pair?: boolean; + /** Set to true if plain DNS is allowed for incoming requests. */ + serve_plain_dns?: boolean; + /** DNS-over-HTTPS port. If 0, DNSCrypt will be disabled. */ + port_dnscrypt?: number; + /** Path to the DNSCrypt configuration file. */ + dnscrypt_config_file?: string; +} diff --git a/client_v2/src/api/model/tlsConfigBody.ts b/client_v2/src/api/model/tlsConfigBody.ts new file mode 100644 index 000000000..a5cea42f2 --- /dev/null +++ b/client_v2/src/api/model/tlsConfigBody.ts @@ -0,0 +1,6 @@ +import type { TlsConfig } from './tlsConfig'; + +/** + * TLS configuration JSON + */ +export type TlsConfigBody = TlsConfig; diff --git a/client_v2/src/api/model/tlsConfigKeyType.ts b/client_v2/src/api/model/tlsConfigKeyType.ts new file mode 100644 index 000000000..a0343b6ee --- /dev/null +++ b/client_v2/src/api/model/tlsConfigKeyType.ts @@ -0,0 +1,4 @@ +/** + * Key type. + */ +export type TlsConfigKeyType = 'RSA' | 'ECDSA'; diff --git a/client_v2/src/api/model/topArrayEntry.ts b/client_v2/src/api/model/topArrayEntry.ts new file mode 100644 index 000000000..f0a69923d --- /dev/null +++ b/client_v2/src/api/model/topArrayEntry.ts @@ -0,0 +1,7 @@ +/** + * Represent the number of hits or time duration per key (url, domain, or client IP). + */ +export interface TopArrayEntry { + domain_or_ip?: number; + [key: string]: unknown; +} diff --git a/client_v2/src/api/model/upstreamsConfig.ts b/client_v2/src/api/model/upstreamsConfig.ts new file mode 100644 index 000000000..2691092bd --- /dev/null +++ b/client_v2/src/api/model/upstreamsConfig.ts @@ -0,0 +1,13 @@ +/** + * Upstream configuration to be tested + */ +export interface UpstreamsConfig { + /** Bootstrap DNS servers, port is optional after colon. */ + bootstrap_dns: string[]; + /** Upstream DNS servers, port is optional after colon. */ + upstream_dns: string[]; + /** Fallback DNS servers, port is optional after colon. */ + fallback_dns?: string[]; + /** Local PTR resolvers, port is optional after colon. */ + private_upstream?: string[]; +} diff --git a/client_v2/src/api/model/upstreamsConfigResponse.ts b/client_v2/src/api/model/upstreamsConfigResponse.ts new file mode 100644 index 000000000..69bc8a806 --- /dev/null +++ b/client_v2/src/api/model/upstreamsConfigResponse.ts @@ -0,0 +1,6 @@ +/** + * Upstreams configuration response + */ +export interface UpstreamsConfigResponse { + [key: string]: string; +} diff --git a/client_v2/src/api/model/versionInfo.ts b/client_v2/src/api/model/versionInfo.ts new file mode 100644 index 000000000..c03f484d0 --- /dev/null +++ b/client_v2/src/api/model/versionInfo.ts @@ -0,0 +1,11 @@ +/** + * Information about the latest available version of AdGuard Home. + */ +export interface VersionInfo { + /** If true then other fields doesn't appear. */ + disabled: boolean; + new_version?: string; + announcement?: string; + announcement_url?: string; + can_autoupdate?: boolean; +} diff --git a/client_v2/src/api/model/whoisInfo.ts b/client_v2/src/api/model/whoisInfo.ts new file mode 100644 index 000000000..0f514a723 --- /dev/null +++ b/client_v2/src/api/model/whoisInfo.ts @@ -0,0 +1,3 @@ +export interface WhoisInfo { + [key: string]: string; +} diff --git a/client_v2/src/common/controls/Input/PasswordInput.tsx b/client_v2/src/common/controls/Input/PasswordInput.tsx index b4e781e2a..d67f87e5c 100644 --- a/client_v2/src/common/controls/Input/PasswordInput.tsx +++ b/client_v2/src/common/controls/Input/PasswordInput.tsx @@ -44,7 +44,7 @@ export const PasswordInput = (props: Props) => { onMouseDown={(e: MouseEvent) => e.preventDefault()} onClick={() => setIsPasswordVisible((v) => !v)} > - +
} diff --git a/client_v2/src/common/controls/Radio/Radio.tsx b/client_v2/src/common/controls/Radio/Radio.tsx index cd10714f4..0b1cdf9fc 100644 --- a/client_v2/src/common/controls/Radio/Radio.tsx +++ b/client_v2/src/common/controls/Radio/Radio.tsx @@ -52,7 +52,7 @@ export const Radio = (props: Props />
diff --git a/client_v2/src/common/controls/Select/CheckIcons.tsx b/client_v2/src/common/controls/Select/CheckIcons.tsx index a3e195557..d8626786e 100644 --- a/client_v2/src/common/controls/Select/CheckIcons.tsx +++ b/client_v2/src/common/controls/Select/CheckIcons.tsx @@ -1,19 +1,14 @@ -import { type JSX } from 'solid-js'; import { useSelectItemContext, useComboboxItemContext } from '@ark-ui/solid'; import { Icon } from 'panel/common/ui/Icon'; -const renderCheckIcon = (selected: boolean): JSX.Element => ( - -); - // Check/dot icon for ArkSelect.Item (reads context reactively in JSX). export const SelectCheckIcon = () => { const itemCtx = useSelectItemContext(); - return renderCheckIcon(itemCtx().selected); + return ; }; // Check/dot icon for ArkCombobox.Item. export const ComboboxCheckIcon = () => { const itemCtx = useComboboxItemContext(); - return renderCheckIcon(itemCtx().selected); + return ; }; diff --git a/client_v2/src/common/controls/Select/Select.pcss b/client_v2/src/common/controls/Select/Select.pcss index 4310fbe6a..438afad03 100644 --- a/client_v2/src/common/controls/Select/Select.pcss +++ b/client_v2/src/common/controls/Select/Select.pcss @@ -189,7 +189,7 @@ font-size: 16px; background-color: var(--default-page-background); display: flex; - align-items: center; + align-items: stretch; &:hover, &:focus-within { @@ -400,6 +400,7 @@ flex: 1; min-width: 0; padding: 8px 12px 8px 0; + align-items: center; [data-scope='combobox'][data-part='input'] { flex: 1 1 80px; @@ -429,7 +430,7 @@ display: grid; flex: 1; min-width: 0; - align-items: center; + align-items: stretch; .solid-combobox-single-value, .solid-select-placeholder, @@ -440,6 +441,7 @@ .solid-combobox-single-value, .solid-select-placeholder { + align-self: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; @@ -458,6 +460,7 @@ [data-scope='combobox'][data-part='input'] { width: 100%; + height: 100%; min-width: 0; border: none; background: transparent; diff --git a/client_v2/src/common/controls/Select/Select.tsx b/client_v2/src/common/controls/Select/Select.tsx index 0083032bd..722e29a12 100644 --- a/client_v2/src/common/controls/Select/Select.tsx +++ b/client_v2/src/common/controls/Select/Select.tsx @@ -131,7 +131,7 @@ export const Select = < const testId = (option: any) => getItemTestId(props.optionTestIdPrefix, option.value); - const clearAriaLabel = intl.getMessage('clear_btn'); + const clearAriaLabel = createMemo(() => intl.getMessage('clear_btn')); // Reads the label from the controlled value prop (option object). // Falls back to pendingLabel (set on selection) before props.value updates. @@ -196,7 +196,7 @@ export const Select = < - + @@ -303,17 +303,7 @@ export const Select = < > - { - // Click the input when clicking the control's dead zone. - if (e.target === e.currentTarget) { - const input = (e.currentTarget as HTMLElement).querySelector( - 'input', - ); - input?.click(); - } - }} - > +
{/* Clear button shown whenever values exist. */} 0}> - + @@ -364,7 +354,7 @@ export const Select = < />
0}> - + diff --git a/client_v2/src/common/controls/Textarea/styles.module.pcss b/client_v2/src/common/controls/Textarea/styles.module.pcss index 70bfac491..654546dfd 100644 --- a/client_v2/src/common/controls/Textarea/styles.module.pcss +++ b/client_v2/src/common/controls/Textarea/styles.module.pcss @@ -70,11 +70,6 @@ transition: border var(--t2), background-color var(--t2); - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } &:focus-within { border-color: var(--pressed-link); diff --git a/client_v2/src/common/intl/index.ts b/client_v2/src/common/intl/index.ts index ab808bb3f..5caad6d5a 100644 --- a/client_v2/src/common/intl/index.ts +++ b/client_v2/src/common/intl/index.ts @@ -1,47 +1,48 @@ import { createSignal } from 'solid-js'; -import { I18nInterface, translate } from '@adguard/translate'; +import { I18nInterface, Locale, translate } from '@adguard/translate'; import { BASE_LOCALE } from 'panel/helpers/twosky'; - -import en from 'panel/__locales/en.json'; -import de from 'panel/__locales/de.json'; -import es from 'panel/__locales/es.json'; -import fr from 'panel/__locales/fr.json'; -import it from 'panel/__locales/it.json'; -import ja from 'panel/__locales/ja.json'; -import ko from 'panel/__locales/ko.json'; -import ptBr from 'panel/__locales/pt-br.json'; -import ptPt from 'panel/__locales/pt-pt.json'; -import ru from 'panel/__locales/ru.json'; -import zhCn from 'panel/__locales/zh-cn.json'; -import zhTw from 'panel/__locales/zh-tw.json'; import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper'; +import { LANGUAGE_QUERY_PARAM } from 'panel/helpers/constants'; + +import { + LOCALES, + LOCALE_LOADERS, + LOCALE_CODES, + LocaleMessage, +} from 'panel/common/intl/locales.generated'; export type LocalesType = ReturnType; -type LocalesTypes = Partial>>; +/** + * The live message map — starts with only the base locale (`en`), + * populated with additional locales at runtime via {@link preloadLocale}. + */ +const messages: Record = { ...LOCALES }; -const LOCALES = { - en, - de, - es, - fr, - it, - ja, - ko, - ru, - 'pt-br': ptBr, - 'pt-pt': ptPt, - 'zh-cn': zhCn, - 'zh-tw': zhTw, +/** + * Converts a hyphenated twosky locale code to the underscore format that + * {@link https://github.com/AdguardTeam/translate @adguard/translate} + * expects for plural-form lookups (e.g. pt-br → pt_br). + */ +const toTranslateLocale = (code: string): Locale => { + // si-lk → am (Amharic): identical CLDR plural rules (one: n=0..1, other). + // This only affects plural-form indexing — actual strings come from si-lk.json. + // + // TODO(ik): Contribute missing `si` locale to @adguard/translate + if (code === 'si-lk') return 'am' as Locale; + + // zh-hk / sr-cs → parent locale + if (code === 'zh-hk') return 'zh' as Locale; + if (code === 'sr-cs') return 'sr' as Locale; + + return code.replace(/-/g, '_') as Locale; }; -const messages: LocalesTypes = LOCALES; - const resolveLanguage = (lng: string): LocalesType => { const l = lng.toLowerCase(); - if (messages[l as LocalesType]) { + if (LOCALE_CODES.has(l)) { return l as LocalesType; } @@ -62,9 +63,9 @@ const resolveLanguage = (lng: string): LocalesType => { } // Try base language (e.g., en-us -> en) - const base = l.split('-')[0] as LocalesType; - if (messages[base]) { - return base; + const base = l.split('-')[0]; + if (LOCALE_CODES.has(base)) { + return base as LocalesType; } return BASE_LOCALE as LocalesType; @@ -74,22 +75,47 @@ export const i18n = (lang: LocalesType) => { const resolved = resolveLanguage(lang); return { getMessage: (key: string) => messages[resolved]?.[key] || '', - getUILanguage: () => resolved, + getUILanguage: () => toTranslateLocale(resolved), getBaseMessage: (key: string) => messages.en![key] || key, getBaseUILanguage: () => BASE_LOCALE as LocalesType, }; }; -const detectedLanguage = ((typeof window !== 'undefined' && - typeof localStorage !== 'undefined' && - LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) || - (typeof navigator !== 'undefined' && (navigator.language as string)) || - BASE_LOCALE) as LocalesType; +/** + * Detects the initial language using a priority chain: URL query param + * (set after cross-port redirect from the install wizard) → localStorage + * → browser language → base locale. + */ +export const getInitialLanguage = (): LocalesType => { + if (typeof window === 'undefined') { + return BASE_LOCALE as LocalesType; + } -const initialLanguage: LocalesType = resolveLanguage(detectedLanguage); + const urlLang = new URL(window.location.href).searchParams.get(LANGUAGE_QUERY_PARAM); + if (urlLang) { + const resolved = resolveLanguage(urlLang); + LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, resolved); + return resolved; + } -// Solid-reactive language signal -const [lang, setLang] = createSignal(initialLanguage); + const stored = LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE); + if (stored) { + return resolveLanguage(stored); + } + + if (typeof navigator !== 'undefined' && navigator.language) { + return resolveLanguage(navigator.language); + } + + return BASE_LOCALE as LocalesType; +}; + +export const initialLanguage: LocalesType = getInitialLanguage(); + +// Always start with English (the only locale guaranteed to be in `messages` +// at module-init time). The App.onMount preloads the actual detected locale +// and triggers a reactive re-render via changeLanguage. +const [lang, setLang] = createSignal(BASE_LOCALE as LocalesType); /** * Creates default value functions for common HTML tags. @@ -182,7 +208,7 @@ const createSolidTranslator = (i18nInstance: ReturnType) => createSolidDefaultValues(), ); -let translator = createSolidTranslator(i18n(initialLanguage)); +let translator = createSolidTranslator(i18n(BASE_LOCALE as LocalesType)); const intl = { getMessage: (key: string, values?: any) => { @@ -215,8 +241,20 @@ const intl = { getBaseUILanguage: () => BASE_LOCALE as LocalesType, - changeLanguage: (newLang: LocalesType) => { + /** + * Changes the active language. If the locale has not been loaded yet + * (common at boot), it is fetched on-demand via the lazy + * `LOCALE_LOADERS` map before the translator is rebuilt. + */ + changeLanguage: async (newLang: LocalesType) => { const resolved = resolveLanguage(newLang); + try { + await preloadLocale(resolved); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('[i18n] Failed to load locale:', resolved, err); + return; + } translator = createSolidTranslator(i18n(resolved)); setLang(resolved); }, @@ -226,4 +264,28 @@ const intl = { }, }; +/** + * Loads a non-base locale into the `messages` map so that subsequent + * `getMessage`/`getPlural` calls can serve it. If the locale is already + * loaded or is the base locale (`en`), this is a no-op. + */ +export const preloadLocale = async (code: string) => { + if (messages[code]) return; + const loader = LOCALE_LOADERS[code]; + if (!loader) return; + const mod = await loader(); + messages[code] = (mod as { default?: LocaleMessage }).default ?? (mod as LocaleMessage); +}; + +// Fire-and-forget: preload the browser-detected locale as soon as the module +// loads. Every entry point (dashboard, login, install, forgot_password) +// benefits without needing its own onMount preload gate. The app renders +// immediately with English; once the locale chunk loads, changeLanguage +// triggers a reactive re-render. +if (typeof window !== 'undefined') { + intl.changeLanguage(initialLanguage).catch((err) => { + console.warn('[i18n] Failed to preload locale:', err); + }); +} + export default intl; diff --git a/client_v2/src/common/intl/locales.generated.ts b/client_v2/src/common/intl/locales.generated.ts new file mode 100644 index 000000000..a69c93b5c --- /dev/null +++ b/client_v2/src/common/intl/locales.generated.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +// AUTO-GENERATED by `npm run locales:generate`. Do not edit by hand. +// Source: .twosky.json → project_id: home_v2 → languages +// Excludes: (none) + +export type LocaleMessage = Record; + +import en from 'panel/__locales/en.json'; + +export const LOCALE_LOADERS: Record Promise> = { + ar: () => import(/* webpackChunkName: "locale.ar" */ 'panel/__locales/ar.json'), + be: () => import(/* webpackChunkName: "locale.be" */ 'panel/__locales/be.json'), + bg: () => import(/* webpackChunkName: "locale.bg" */ 'panel/__locales/bg.json'), + cs: () => import(/* webpackChunkName: "locale.cs" */ 'panel/__locales/cs.json'), + da: () => import(/* webpackChunkName: "locale.da" */ 'panel/__locales/da.json'), + de: () => import(/* webpackChunkName: "locale.de" */ 'panel/__locales/de.json'), + es: () => import(/* webpackChunkName: "locale.es" */ 'panel/__locales/es.json'), + fa: () => import(/* webpackChunkName: "locale.fa" */ 'panel/__locales/fa.json'), + fi: () => import(/* webpackChunkName: "locale.fi" */ 'panel/__locales/fi.json'), + fr: () => import(/* webpackChunkName: "locale.fr" */ 'panel/__locales/fr.json'), + hr: () => import(/* webpackChunkName: "locale.hr" */ 'panel/__locales/hr.json'), + hu: () => import(/* webpackChunkName: "locale.hu" */ 'panel/__locales/hu.json'), + id: () => import(/* webpackChunkName: "locale.id" */ 'panel/__locales/id.json'), + it: () => import(/* webpackChunkName: "locale.it" */ 'panel/__locales/it.json'), + ja: () => import(/* webpackChunkName: "locale.ja" */ 'panel/__locales/ja.json'), + ko: () => import(/* webpackChunkName: "locale.ko" */ 'panel/__locales/ko.json'), + nl: () => import(/* webpackChunkName: "locale.nl" */ 'panel/__locales/nl.json'), + no: () => import(/* webpackChunkName: "locale.no" */ 'panel/__locales/no.json'), + pl: () => import(/* webpackChunkName: "locale.pl" */ 'panel/__locales/pl.json'), + 'pt-br': () => import(/* webpackChunkName: "locale.pt-br" */ 'panel/__locales/pt-br.json'), + 'pt-pt': () => import(/* webpackChunkName: "locale.pt-pt" */ 'panel/__locales/pt-pt.json'), + ro: () => import(/* webpackChunkName: "locale.ro" */ 'panel/__locales/ro.json'), + ru: () => import(/* webpackChunkName: "locale.ru" */ 'panel/__locales/ru.json'), + 'si-lk': () => import(/* webpackChunkName: "locale.si-lk" */ 'panel/__locales/si-lk.json'), + sk: () => import(/* webpackChunkName: "locale.sk" */ 'panel/__locales/sk.json'), + sl: () => import(/* webpackChunkName: "locale.sl" */ 'panel/__locales/sl.json'), + 'sr-cs': () => import(/* webpackChunkName: "locale.sr-cs" */ 'panel/__locales/sr-cs.json'), + sv: () => import(/* webpackChunkName: "locale.sv" */ 'panel/__locales/sv.json'), + th: () => import(/* webpackChunkName: "locale.th" */ 'panel/__locales/th.json'), + tr: () => import(/* webpackChunkName: "locale.tr" */ 'panel/__locales/tr.json'), + uk: () => import(/* webpackChunkName: "locale.uk" */ 'panel/__locales/uk.json'), + vi: () => import(/* webpackChunkName: "locale.vi" */ 'panel/__locales/vi.json'), + 'zh-cn': () => import(/* webpackChunkName: "locale.zh-cn" */ 'panel/__locales/zh-cn.json'), + 'zh-hk': () => import(/* webpackChunkName: "locale.zh-hk" */ 'panel/__locales/zh-hk.json'), + 'zh-tw': () => import(/* webpackChunkName: "locale.zh-tw" */ 'panel/__locales/zh-tw.json'), +}; + +export const LOCALE_CODES = new Set([ + 'ar', + 'be', + 'bg', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fa', + 'fi', + 'fr', + 'hr', + 'hu', + 'id', + 'it', + 'ja', + 'ko', + 'nl', + 'no', + 'pl', + 'pt-br', + 'pt-pt', + 'ro', + 'ru', + 'si-lk', + 'sk', + 'sl', + 'sr-cs', + 'sv', + 'th', + 'tr', + 'uk', + 'vi', + 'zh-cn', + 'zh-hk', + 'zh-tw', +]); + +export const LOCALES: Record = { + en: en, +}; diff --git a/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx index 6a7eff301..734ceaaaf 100644 --- a/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx +++ b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx @@ -13,7 +13,7 @@ type Props = { spacing?: boolean; menuClass?: string; overlayClass?: string; - position?: 'bottomLeft' | 'bottomRight' | 'bottom'; + position?: 'bottomLeft' | 'bottomRight'; }; export const FaqTooltip = (props: Props) => { @@ -33,7 +33,7 @@ export const FaqTooltip = (props: Props) => { } class={s.dropdown} - position={position() as any} + position={position()} >
e.stopPropagation()}> diff --git a/client_v2/src/common/ui/Footer/Footer.tsx b/client_v2/src/common/ui/Footer/Footer.tsx index 1c643cc1a..3f21d4e24 100644 --- a/client_v2/src/common/ui/Footer/Footer.tsx +++ b/client_v2/src/common/ui/Footer/Footer.tsx @@ -4,7 +4,7 @@ import cn from 'clsx'; import theme from 'panel/lib/theme'; import { Dropdown } from 'panel/common/ui/Dropdown'; import { Icon } from 'panel/common/ui/Icon'; -import intl, { LocalesType } from 'panel/common/intl'; +import intl, { type LocalesType } from 'panel/common/intl'; import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper'; import { LanguageDropdown } from '../LanguageDropdown/LanguageDropdown'; @@ -19,6 +19,8 @@ import { import { dashboardState } from 'panel/stores/dashboard'; import s from './styles.module.pcss'; +import { Lang } from 'panel/api/model/lang'; +import { ProfileInfoTheme } from 'panel/api/model/profileInfoTheme'; export const Footer = () => { const currentTheme = () => dashboardState.theme || THEMES.auto; @@ -50,18 +52,18 @@ export const Footer = () => { return 'theme_light'; }; - const changeLanguage = async (newLang: LocalesType) => { + const changeLanguage = async (newLang: Lang) => { + await intl.changeLanguage(newLang as LocalesType); setHtmlLangAttr(newLang); + LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang); try { await changeLanguageAction(newLang); - LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang); - window.location.reload(); } catch (error) { console.error('Failed to save language preference:', error); } }; - const onThemeChange = (value: string) => { + const onThemeChange = (value: ProfileInfoTheme) => { if (isLoggedIn()) { changeTheme(value); } else { @@ -127,7 +129,7 @@ export const Footer = () => { onOpenChange={setThemeDropdownOpen} menu={
- + {(v) => (
{props.center}
- + {!props.hideLanguageDropdown && ( + changeLanguage(lang)} + class={props.dropdownClass} + position={props.dropdownPosition ?? 'bottomRight'} + /> + )}
diff --git a/client_v2/src/common/ui/SortSelect/SortSelect.tsx b/client_v2/src/common/ui/SortSelect/SortSelect.tsx index f8a52a63f..09010ed1c 100644 --- a/client_v2/src/common/ui/SortSelect/SortSelect.tsx +++ b/client_v2/src/common/ui/SortSelect/SortSelect.tsx @@ -1,3 +1,4 @@ +import { createMemo } from 'solid-js'; import cn from 'clsx'; import intl from 'panel/common/intl'; @@ -12,16 +13,16 @@ type Props = { }; export const SortSelect = (props: Props) => { - const options: IOption[] = [ + const options = createMemo[]>(() => [ { value: 'asc', label: intl.getMessage('sort_asc') }, { value: 'desc', label: intl.getMessage('sort_desc') }, - ]; + ]); return (
- options={options} - value={options.find((o) => o.value === props.value)} + options={options()} + value={options().find((o) => o.value === props.value)} onChange={(option: any) => props.onChange(option.value as 'asc' | 'desc')} height="medium" isSearchable={false} diff --git a/client_v2/src/components/App/index.tsx b/client_v2/src/components/App/index.tsx index 174f28a2d..b8224bf6f 100644 --- a/client_v2/src/components/App/index.tsx +++ b/client_v2/src/components/App/index.tsx @@ -19,7 +19,7 @@ import { Dashboard } from 'panel/components/Dashboard'; import { Dhcp } from 'panel/components/Dhcp'; import { LeasesPage } from 'panel/components/Dhcp/LeasesPage'; import { QueryLog } from 'panel/components/QueryLog'; -import Toasts from '../Toasts'; +import { Toasts } from 'panel/components/Toasts'; import { THEMES } from '../../helpers/constants'; import { setHtmlLangAttr, setUITheme } from '../../helpers/helpers'; import { getDnsStatus, getTimerStatus, dashboardState } from '../../stores/dashboard'; @@ -67,9 +67,10 @@ const App = () => { const language = dashboardState.language; const processing = dashboardState.processing; if (!processing && language) { - intl.changeLanguage(language as LocalesType); - setHtmlLangAttr(language); - LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, language); + intl.changeLanguage(language as LocalesType).then(() => { + setHtmlLangAttr(language); + LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, language); + }); } }); diff --git a/client_v2/src/components/BlockedServices/BlockedServices.tsx b/client_v2/src/components/BlockedServices/BlockedServices.tsx index 491258609..74759b226 100644 --- a/client_v2/src/components/BlockedServices/BlockedServices.tsx +++ b/client_v2/src/components/BlockedServices/BlockedServices.tsx @@ -147,7 +147,7 @@ export const BlockedServices = (props: Props) => { (servicesState.allServices == null || servicesState.allServices.length === 0) && (servicesState.processingAll || servicesState.processing); const isGloballyDisabled = () => - props.clientScope ? clientFormState.use_global_blocked_services : false; + props.clientScope ? clientFormState.use_global_settings : false; const getScheduleRoute = () => { if (!props.clientScope) { @@ -203,7 +203,12 @@ export const BlockedServices = (props: Props) => {

{intl.getMessage('blocked_services_desc')}

- +
{intl.getMessage('inactivity_schedule')} diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx index ae815224a..dc8ece3c1 100644 --- a/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx +++ b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx @@ -7,6 +7,7 @@ import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog'; import { PageLoader } from 'panel/common/ui/Loader'; import { Select } from 'panel/common/controls/Select'; import { updateClientFormField, clientFormState } from 'panel/stores/clientForm'; +import type { ClientFormState } from 'panel/initialState'; import { getBlockedServices, updateBlockedServices, servicesState } from 'panel/stores/services'; import theme from 'panel/lib/theme'; @@ -47,7 +48,7 @@ export const InactivitySchedule = (props: Props) => { const schedule = createMemo(() => { return props.clientScope ? (clientFormState.blocked_services_schedule as unknown as ScheduleData) - : servicesState.list?.schedule; + : (servicesState.list?.schedule as ScheduleData | undefined); }); const currentTimezone = () => schedule()?.time_zone; @@ -73,7 +74,7 @@ export const InactivitySchedule = (props: Props) => { return; } const newSchedule = props.clientScope - ? { ...(clientFormState.blocked_services_schedule as any), time_zone: option.value } + ? { ...clientFormState.blocked_services_schedule, time_zone: option.value } : { ...schedule(), time_zone: option.value }; if (props.clientScope) { updateClientFormField('blocked_services_schedule', newSchedule, true); @@ -108,7 +109,11 @@ export const InactivitySchedule = (props: Props) => { } }); if (props.clientScope) { - updateClientFormField('blocked_services_schedule', newSchedule, true); + updateClientFormField( + 'blocked_services_schedule', + newSchedule as ClientFormState['blocked_services_schedule'], + true, + ); } else { updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule }); } @@ -124,7 +129,11 @@ export const InactivitySchedule = (props: Props) => { }); newSchedule[day] = { start, end }; if (props.clientScope) { - updateClientFormField('blocked_services_schedule', newSchedule, true); + updateClientFormField( + 'blocked_services_schedule', + newSchedule as ClientFormState['blocked_services_schedule'], + true, + ); } else { updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule }); } @@ -172,7 +181,10 @@ export const InactivitySchedule = (props: Props) => { >
-
+
{ } > {/* clientScope - render inline */} -
+
{intl.getMessage('inactivity_schedule_timezone')}
setRangeStart((e.target as HTMLInputElement).value)} onBlur={validateRangeStart} @@ -94,7 +105,10 @@ export const DhcpV6Modal = (props: Props) => { id="v6_lease_duration" type="number" label={intl.getMessage('dhcp_form_lease_title')} - placeholder="86400" + placeholder={ + v6Placeholders().lease_duration || + DHCP_VALUES_PLACEHOLDERS.ipv6.lease_duration + } value={leaseDuration()} onChange={(e: Event) => setLeaseDuration((e.target as HTMLInputElement).value)} onBlur={() => validateLeaseDuration()} diff --git a/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx index 3451a3269..e063e8657 100644 --- a/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx +++ b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx @@ -37,7 +37,10 @@ export const InterfaceSelector = (props: Props) => { const gatewayIp = () => selectedIface()?.gateway_ip || ''; const hardwareAddress = () => selectedIface()?.hardware_address || ''; - const ipAddresses = () => selectedIface()?.ip_addresses || []; + const ipAddresses = () => { + const iface = selectedIface(); + return [...(iface?.ipv4_addresses || []), ...(iface?.ipv6_addresses || [])]; + }; const displayIps = () => { const ips = ipAddresses(); diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx index c390d6b3a..35c3ddca0 100644 --- a/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx +++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx @@ -1,4 +1,4 @@ -import { createSignal, createEffect, type Accessor, Show } from 'solid-js'; +import { createSignal, createEffect, createMemo, type Accessor, Show } from 'solid-js'; import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig'; import intl from 'panel/common/intl'; @@ -6,6 +6,7 @@ import { ConfigDialog } from 'panel/common/ui/ConfigDialog'; import { Input } from 'panel/common/controls/Input'; import { Radio } from 'panel/common/controls/Radio'; import { BLOCKING_MODES, UINT32_RANGE } from 'panel/helpers/constants'; +import type { DNSConfigBlockingMode } from 'panel/api/model'; import { getBlockingModeOptions } from '../../helpers'; import { validateRequiredValue, @@ -24,7 +25,7 @@ type Props = { }; export const BlockingModeDialog = (props: Props) => { - const blockingModeOptions = getBlockingModeOptions(); + const blockingModeOptions = createMemo(() => getBlockingModeOptions()); const [blockingMode, setBlockingMode] = createSignal(dnsConfigState.blocking_mode); createEffect(() => { @@ -88,9 +89,9 @@ export const BlockingModeDialog = (props: Props) => { > setBlockingMode(v)} + handleChange={(v: DNSConfigBlockingMode) => setBlockingMode(v)} inModal /> diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx index 326f27a19..1a7169285 100644 --- a/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx +++ b/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx @@ -28,6 +28,7 @@ export const BootstrapDnsDialog = (props: Props) => { description={ <>

{intl.getMessage('dns_bootstrap_dns_desc')}

+

{intl.getMessage('dns_bootstrap_dns_desc_2')}

} onClose={props.onClose} diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx index 311b267f2..351e88ade 100644 --- a/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx +++ b/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx @@ -7,6 +7,7 @@ import { validateUpstreams } from 'panel/helpers/validators'; import { useField } from 'panel/hooks/useField'; import { Examples } from './Examples'; import theme from 'panel/lib/theme'; +import { UPSTREAM_CONFIGURATION_WIKI_LINK } from 'panel/helpers/constants'; type Props = { open: Accessor; @@ -32,7 +33,7 @@ export const FallbackDnsDialog = (props: Props) => { {intl.getMessage('dns_fallback_dns_desc_2', { a: (text: string) => ( ; @@ -39,7 +40,7 @@ export const ServerAddressesDialog = (props: Props) => { {intl.getMessage('dns_server_addresses_desc_2', { a: (text: string) => ( { name="upstream_mode" options={upstreamModeOptions()} value={upstreamMode()} - handleChange={(v: string) => setUpstreamMode(v)} + handleChange={(v: DNSConfigUpstreamMode) => setUpstreamMode(v)} inModal /> diff --git a/client_v2/src/components/DnsSettings/helpers.ts b/client_v2/src/components/DnsSettings/helpers.ts index e75efaf7b..6bc087014 100644 --- a/client_v2/src/components/DnsSettings/helpers.ts +++ b/client_v2/src/components/DnsSettings/helpers.ts @@ -1,7 +1,8 @@ import { DNS_REQUEST_OPTIONS, BLOCKING_MODES, EDNS_MODES } from 'panel/helpers/constants'; import intl from 'panel/common/intl'; +import type { DNSConfigBlockingMode, DNSConfigUpstreamMode } from 'panel/api/model'; -export const getUpstreamModeSummary = (mode: string): string => { +export const getUpstreamModeSummary = (mode: DNSConfigUpstreamMode): string => { switch (mode) { case DNS_REQUEST_OPTIONS.PARALLEL: return intl.getMessage('upstream_dns_parallel_requests'); @@ -27,7 +28,7 @@ export const getRateLimitSummary = (ratelimit: number): string => { return intl.getMessage('dns_rate_limit_value', { value: ratelimit }); }; -export const getBlockingModeSummary = (mode: string): string => { +export const getBlockingModeSummary = (mode: DNSConfigBlockingMode): string => { switch (mode) { case BLOCKING_MODES.refused: return 'REFUSED'; diff --git a/client_v2/src/components/Encryption/Encryption.tsx b/client_v2/src/components/Encryption/Encryption.tsx index 2c98d6a12..33dbcc7fe 100644 --- a/client_v2/src/components/Encryption/Encryption.tsx +++ b/client_v2/src/components/Encryption/Encryption.tsx @@ -132,9 +132,9 @@ export const Encryption = () => { serve_plain_dns: encryptionState.serve_plain_dns, server_name: encryptionState.server_name, force_https: encryptionState.force_https, - port_https: encryptionState.port_https, - port_dns_over_tls: encryptionState.port_dns_over_tls, - port_dns_over_quic: encryptionState.port_dns_over_quic, + port_https: Number(encryptionState.port_https) || 0, + port_dns_over_tls: Number(encryptionState.port_dns_over_tls) || 0, + port_dns_over_quic: Number(encryptionState.port_dns_over_quic) || 0, certificate_chain: encryptionState.certificate_chain, private_key: encryptionState.private_key, certificate_path: encryptionState.certificate_path, diff --git a/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx b/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx index 649e12c89..e23e31e02 100644 --- a/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx +++ b/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx @@ -69,9 +69,9 @@ export const AddTlsCertModal = (props: Props) => { enabled: encryptionState.enabled, serve_plain_dns: encryptionState.serve_plain_dns, server_name: encryptionState.server_name, - port_https: encryptionState.port_https || 0, - port_dns_over_tls: encryptionState.port_dns_over_tls || 0, - port_dns_over_quic: encryptionState.port_dns_over_quic || 0, + port_https: Number(encryptionState.port_https) || 0, + port_dns_over_tls: Number(encryptionState.port_dns_over_tls) || 0, + port_dns_over_quic: Number(encryptionState.port_dns_over_quic) || 0, certificate_chain: certChain(), private_key: privateKey(), certificate_path: certPath(), diff --git a/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx b/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx index 6fa50b3bd..456fbe5e1 100644 --- a/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx +++ b/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx @@ -4,7 +4,7 @@ import { Input } from 'panel/common/controls/Input'; import { FaqTooltip } from 'panel/common/ui/FaqTooltip'; import intl from 'panel/common/intl'; import { encryptionState, setTlsConfig } from 'panel/stores/encryption'; -import { toNumber } from 'panel/helpers/form'; +import { toNumber, normalizeServerName } from 'panel/helpers/form'; import { validateServerName, validatePort, validateIsSafePort } from 'panel/helpers/validators'; import s from '../styles.module.pcss'; import theme from 'panel/lib/theme'; @@ -27,9 +27,9 @@ export const ServerSettingsModal = (props: Props) => { (open) => { if (open) { setServerName(encryptionState.server_name || ''); - setPortHttps(encryptionState.port_https || 0); - setPortDot(encryptionState.port_dns_over_tls || 0); - setPortDoq(encryptionState.port_dns_over_quic || 0); + setPortHttps(Number(encryptionState.port_https) || 0); + setPortDot(Number(encryptionState.port_dns_over_tls) || 0); + setPortDoq(Number(encryptionState.port_dns_over_quic) || 0); setErrors({}); } }, @@ -67,7 +67,10 @@ export const ServerSettingsModal = (props: Props) => { }; const handleServerNameBlur = () => { - const err = validateServerName(serverName()); + const normalized = normalizeServerName(serverName()); + setServerName(normalized); + + const err = validateServerName(normalized); setErrors((prev) => { const next = { ...prev }; if (err) { diff --git a/client_v2/src/components/FilterLists/DNSRewrites.tsx b/client_v2/src/components/FilterLists/DNSRewrites.tsx index d769b35e8..e861bc3cf 100644 --- a/client_v2/src/components/FilterLists/DNSRewrites.tsx +++ b/client_v2/src/components/FilterLists/DNSRewrites.tsx @@ -21,11 +21,9 @@ import { RewritesTable } from './blocks/RewritesTable/RewritesTable'; import s from './FilterLists.module.pcss'; -export type Rewrite = { - answer: string; - domain: string; - enabled: boolean; -}; +import type { RewriteEntry } from 'panel/api/model/rewriteEntry'; + +export type Rewrite = RewriteEntry & { enabled?: boolean }; export const DNSRewrites = () => { const [currentRewrite, setCurrentRewrite] = createSignal({ diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx index b6373179f..f4515b7ce 100644 --- a/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx @@ -8,6 +8,7 @@ import { ModalWrapper } from 'panel/common/ui/ModalWrapper'; import { closeModal } from 'panel/stores/modals'; import theme from 'panel/lib/theme'; import { Button } from 'panel/common/ui/Button'; +import { InlineLoader } from 'panel/common/ui/Loader/InlineLoader'; import { addFilter, editFilter, filteringState } from 'panel/stores/filtering'; import { Input } from 'panel/common/controls/Input'; import { validatePath, validateRequiredValue } from 'panel/helpers/validators'; @@ -68,11 +69,16 @@ export const ConfigureAllowlistModal = (props: Props) => { switch (props.modalId) { case MODAL_TYPE.ADD_ALLOWLIST: { - addFilter(values.url, values.name, true); + await addFilter(values.url, values.name, true); break; } case MODAL_TYPE.EDIT_ALLOWLIST: { - editFilter(props.filterToEdit!.url, values, true); + if (!props.filterToEdit) return; + await editFilter( + props.filterToEdit.url, + { name: values.name, url: values.url, enabled: props.filterToEdit.enabled ?? true }, + true, + ); break; } default: { @@ -131,6 +137,9 @@ export const ConfigureAllowlistModal = (props: Props) => { variant="primary" size="small" disabled={filteringState.processingAddFilter} + leftAddon={ + filteringState.processingAddFilter ? : undefined + } class={theme.dialog.button} > {getButtonText(props.modalId)} diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx index 73977723c..0475c0230 100644 --- a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx @@ -16,6 +16,7 @@ import { filteringState, } from 'panel/stores/filtering'; import type { Filter } from 'panel/helpers/helpers'; +import type { FilterSetUrlData } from 'panel/api/model/filterSetUrlData'; import { validatePath, validateRequiredValue } from 'panel/helpers/validators'; import { ManualFilterForm } from 'panel/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm'; import { Tabs } from 'panel/common/ui/Tabs'; @@ -170,7 +171,7 @@ export const ConfigureBlocklistModal = (props: Props) => { break; } case MODAL_TYPE.EDIT_BLOCKLIST: { - editFilter(props.filterToEdit!.url, values, false); + editFilter(props.filterToEdit!.url, values as FilterSetUrlData, false); break; } default: { diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx index a03d84079..bd6bf8cc6 100644 --- a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx @@ -20,7 +20,7 @@ import { import { DomainFaqTooltip } from './DomainFaqTooltip'; import { AnswerFaqTooltip } from './AnswerFaqTooltip'; -type FormValues = { +export type FormValues = { answer: string; domain: string; enabled: boolean; @@ -30,7 +30,7 @@ type ConfigureRewritesModalIdType = 'ADD_REWRITE' | 'EDIT_REWRITE'; type Props = { modalId: ConfigureRewritesModalIdType; - rewriteToEdit?: FormValues; + rewriteToEdit?: Partial; onSubmit?: (values: FormValues) => boolean | void | Promise; onClose?: () => void; }; @@ -91,7 +91,11 @@ export const ConfigureRewritesModal = (props: Props) => { validateRequiredValue(answer()) || validateAnswer(answer()) || validateRewriteNotSame(domain(), answer()) || - validateRewriteNotExists(domain(), rewritesState.list, props.rewriteToEdit?.domain); + validateRewriteNotExists( + domain(), + rewritesState.list as { domain: string }[], + props.rewriteToEdit?.domain, + ); setAnswerError(err || undefined); return !err; }; diff --git a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx index 89131b880..25406cf9f 100644 --- a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx @@ -9,9 +9,9 @@ import { deleteRewrite, rewritesState } from 'panel/stores/rewrites'; type Props = { rewriteToDelete: { - answer: string; - domain: string; - enabled: boolean; + answer?: string; + domain?: string; + enabled?: boolean; }; setRewriteToDelete: (value: { answer: string; domain: string; enabled: boolean }) => void; onConfirm?: () => boolean | void | Promise; diff --git a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx index 7fd040e54..be813ca37 100644 --- a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx @@ -8,7 +8,7 @@ import theme from 'panel/lib/theme'; import { setFiltersConfig, filteringState } from 'panel/stores/filtering'; import { ModalWrapper } from 'panel/common/ui/ModalWrapper'; import { MODAL_TYPE } from 'panel/helpers/constants'; -import { closeModal } from 'panel/stores/modals'; +import { closeModal, modalsState } from 'panel/stores/modals'; import { FilterIntervalInput, FILTER_INTERVAL_RANGE } from './FilterIntervalInput'; export const FILTER_INTERVALS = { @@ -65,6 +65,7 @@ export const FilterUpdateModal = () => { ); createEffect(() => { + if (modalsState.modalId !== MODAL_TYPE.FILTER_UPDATE) return; const currentInterval = filteringState.interval; const custom = currentInterval != null && !PREDEFINED_INTERVALS.includes(currentInterval); setIntervalValue(custom ? FILTER_INTERVALS.CUSTOM : (currentInterval ?? 24)); diff --git a/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx index e6aab198c..ec7fd0ce0 100644 --- a/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx +++ b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx @@ -41,7 +41,8 @@ export const ListsTable = (props: Props) => { const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); const pageSize = createMemo( - () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined, + () => + LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined, ); const sortedFilters = createMemo(() => { @@ -93,9 +94,7 @@ export const ListsTable = (props: Props) => { return (
- - {intl.getMessage('name_label')} - + {intl.getMessage('name_label')}
{value} diff --git a/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx b/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx index 32a100475..588ae9044 100644 --- a/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx +++ b/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx @@ -28,7 +28,8 @@ export const RewritesTable = (props: Props) => { const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); const pageSize = createMemo( - () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined, + () => + LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined, ); const sortedList = createMemo(() => { diff --git a/client_v2/src/components/QueryLog/QueryLog.tsx b/client_v2/src/components/QueryLog/QueryLog.tsx index d9d477118..1b67e4d99 100644 --- a/client_v2/src/components/QueryLog/QueryLog.tsx +++ b/client_v2/src/components/QueryLog/QueryLog.tsx @@ -31,7 +31,7 @@ import { getLogsUrlParams } from 'panel/helpers/helpers'; import { RoutePath, linkPathBuilder } from 'panel/components/Routes/Paths'; import { filterLogsByStatus } from './helpers'; -import { LogEntry } from './types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { Header } from './blocks/Header'; import { EmptyState, type EmptyStateMode } from './blocks/EmptyState/EmptyState'; import { LogTable } from './blocks/LogTable'; @@ -56,7 +56,7 @@ export const QueryLog = () => { const navigate = useNavigate(); const location = useLocation(); - const [selectedEntry, setSelectedEntry] = createSignal(null); + const [selectedEntry, setSelectedEntry] = createSignal(null); const [disallowTarget, setDisallowTarget] = createSignal(null); const [isIncrementalLoad, setIsIncrementalLoad] = createSignal(false); @@ -109,7 +109,11 @@ export const QueryLog = () => { (dashboardState.clients || []).flatMap( (persistentClient: any) => persistentClient.ids ?? [], ); - const visibleLogs = () => filterLogsByStatus(queryLogsState.logs || [], currentStatus()); + const visibleLogs = () => + filterLogsByStatus( + queryLogsState.logs || [], + currentStatus(), + ); const emptyStateMode = () => getEmptyStateMode(queryLogsState.enabled, queryLogsState.interval); const hasMore = () => !queryLogsState.isEntireLog; const logs = () => queryLogsState.logs || []; @@ -186,7 +190,7 @@ export const QueryLog = () => { setDisallowTarget(null); }; - const handleRowClick = (entry: LogEntry) => { + const handleRowClick = (entry: NormalizedQueryLogItem) => { setSelectedEntry(entry); }; diff --git a/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx index e4c52742a..849a27543 100644 --- a/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx +++ b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx @@ -25,12 +25,13 @@ import { formatLogTimeDetailed, formatLogDate, } from '../../helpers'; -import { LogEntry, ResponseEntry, Service } from '../../types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from '../../types'; import s from './DetailModal.module.pcss'; type Props = { - entry: LogEntry; + entry: NormalizedQueryLogItem; filters: Filter[]; services: Service[]; whitelistFilters: Filter[]; @@ -40,7 +41,7 @@ type Props = { onAllowService: (serviceId: string) => void; }; -const formatResponses = (responses: ResponseEntry[] = []) => +const formatResponses = (responses: { value?: string; type?: string; ttl?: number }[] = []) => responses .map(({ type, value, ttl }) => { if (!value) { @@ -89,6 +90,8 @@ export const DetailModal = (props: Props) => { const responseList = () => formatResponses(props.entry.response); const originalResponseList = () => formatResponses(props.entry.originalResponse); const trackerSource = () => props.entry.tracker?.sourceData; + const trackerName = () => props.entry.tracker?.name; + const trackerCategory = () => props.entry.tracker?.category; const country = () => props.entry.client_info?.whois?.country; const network = () => props.entry.client_info?.whois?.orgname; const serviceId = () => props.entry.serviceName || props.entry.service_name; @@ -121,10 +124,11 @@ export const DetailModal = (props: Props) => { }; const handleAllowService = () => { - if (!serviceId()) { + const sid = serviceId(); + if (!sid) { return; } - props.onAllowService(serviceId()!); + props.onAllowService(sid); props.onClose(); }; @@ -215,49 +219,63 @@ export const DetailModal = (props: Props) => {

{intl.getMessage('known_tracker')}

-
- {intl.getMessage('query_log_detail_name', { - value: props.entry.tracker!.name, - span: renderValue, - })} -
-
- {intl.getMessage('query_log_detail_category', { - value: props.entry.tracker!.category, - span: renderValue, - })} -
- -
+ + {(name) => ( +
+ {intl.getMessage('query_log_detail_name', { + value: name(), + span: renderValue, + })} +
+ )} +
+ + {(category) => ( +
+ {intl.getMessage('query_log_detail_category', { + value: category(), + span: renderValue, + })} +
+ )} +
+ + {(source) => ( + + {(name) => ( +
+ )} + + )}
diff --git a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx index 7e987e56c..e70c43309 100644 --- a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx @@ -20,17 +20,18 @@ import { hasPersistentClient, isBlockedReason, } from '../../helpers'; -import { LogEntry, Service } from '../../types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from '../../types'; import { ActionsMenu } from '../ActionsMenu'; import s from './LogCard.module.pcss'; type Props = { - entry: LogEntry; + entry: NormalizedQueryLogItem; filters: Filter[]; services: Service[]; whitelistFilters: Filter[]; - onRowClick: (entry: LogEntry) => void; + onRowClick: (entry: NormalizedQueryLogItem) => void; onBlock: (domain: string) => void; onUnblock: (domain: string) => void; onBlockClient: (domain: string, client: string) => void; diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx index 9ffaba907..7c83889fc 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx @@ -4,7 +4,8 @@ import intl from 'panel/common/intl'; import { Loader } from 'panel/common/ui/Loader'; import { Table, TableColumn } from 'panel/common/ui/Table/Table'; -import { LogEntry, Service } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from 'panel/components/QueryLog/types'; import { hasPersistentClient, isBlockedReason } from 'panel/components/QueryLog/helpers'; import { Filter } from 'panel/helpers/helpers'; @@ -16,7 +17,7 @@ import s from './LogTable.module.pcss'; import { ActionsMenu } from '../ActionsMenu'; type Props = { - logs: LogEntry[]; + logs: NormalizedQueryLogItem[]; emptyStateMode: EmptyStateMode; hasMore: boolean; isLoadingMore: boolean; @@ -25,7 +26,7 @@ type Props = { isFilterReloading: boolean; infiniteScrollResetToken: string; onLoadMore: () => void; - onRowClick: (entry: LogEntry) => void; + onRowClick: (entry: NormalizedQueryLogItem) => void; onBlock: (domain: string) => void; onUnblock: (domain: string) => void; onBlockClient: (domain: string, client: string) => void; @@ -45,31 +46,31 @@ export const LogTable = (props: Props) => { untrack(() => props.onSearchSelect(value)); }; - const columns = createMemo[]>(() => [ + const columns = createMemo[]>(() => [ { key: 'time', header: { text: intl.getMessage('time_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , width: 116, sortable: false, }, { key: 'domain', header: { text: intl.getMessage('request_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , sortable: false, }, { key: 'status', header: { text: intl.getMessage('status_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , width: 'minmax(108px, 0.7fr)', sortable: false, }, { key: 'reason', header: { text: intl.getMessage('reason_table_header') }, - render: (_value: unknown, row: LogEntry) => { + render: (_value: unknown, row: NormalizedQueryLogItem) => { return ( { { key: 'client', header: { text: intl.getMessage('client_table_header') }, - render: (_value: unknown, row: LogEntry) => ( + render: (_value: unknown, row: NormalizedQueryLogItem) => ( ), sortable: false, @@ -93,7 +94,7 @@ export const LogTable = (props: Props) => { { key: 'actions', header: { text: '', render: () => null }, - render: (_value: unknown, row: LogEntry) => ( + render: (_value: unknown, row: NormalizedQueryLogItem) => (
(event: MouseEvent) => void; - row: LogEntry; + row: NormalizedQueryLogItem; }; export const ClientCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx index 53e0ba848..5cc090f26 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx @@ -4,17 +4,17 @@ import cn from 'clsx'; import intl from 'panel/common/intl'; import { captitalizeWords } from 'panel/helpers/helpers'; import theme from 'panel/lib/theme'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { formatLogDate, formatLogTimeDetailed, getProtocolName, } from 'panel/components/QueryLog/helpers'; -import { LogEntry } from 'panel/components/QueryLog/types'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; const renderValue = (value: any) => ( @@ -23,6 +23,8 @@ const renderValue = (value: any) => ( export const QueryDetailsTooltipContent = (props: Props) => { const trackerSource = () => props.row.tracker?.sourceData; + const trackerName = () => props.row.tracker?.name; + const trackerCategory = () => props.row.tracker?.category; const displayDomain = () => props.row.unicodeName || props.row.domain; return ( @@ -77,42 +79,56 @@ export const QueryDetailsTooltipContent = (props: Props) => {
-
- {intl.getMessage('query_log_detail_name', { - value: props.row.tracker!.name, - span: renderValue, - })} -
-
- {intl.getMessage('query_log_detail_category', { - value: captitalizeWords(props.row.tracker!.category), - span: renderValue, - })} -
- -
- {intl.getMessage('query_log_detail_source', { - value: trackerSource()!.name, - span: (content: any) => - trackerSource()!.url ? ( - - {content} - - ) : ( - - {content} - - ), - })} -
+ + {(name) => ( +
+ {intl.getMessage('query_log_detail_name', { + value: name(), + span: renderValue, + })} +
+ )} +
+ + {(category) => ( +
+ {intl.getMessage('query_log_detail_category', { + value: captitalizeWords(category()), + span: renderValue, + })} +
+ )} +
+ + {(source) => ( + + {(name) => ( +
+ {intl.getMessage('query_log_detail_source', { + value: name(), + span: (content: any) => + source()?.url ? ( + + {content} + + ) : ( + + {content} + + ), + })} +
+ )} +
+ )}
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx index 00e91686f..9c0443e93 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx @@ -2,7 +2,8 @@ import cn from 'clsx'; import theme from 'panel/lib/theme'; import { Filter } from 'panel/helpers/helpers'; -import { LogEntry, Service } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from 'panel/components/QueryLog/types'; import { getQueryReasonLabel, getQueryReasonDetails, @@ -12,7 +13,7 @@ import { import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; filters: Filter[]; services: Service[]; whitelistFilters: Filter[]; diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx index 7bf0801b4..991d2f430 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx @@ -7,12 +7,12 @@ import theme from 'panel/lib/theme'; import { Icon } from 'panel/common/ui/Icon'; import { getProtocolName } from 'panel/components/QueryLog/helpers'; import { QueryDetailsTooltipContent } from 'panel/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const RequestCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx index 27f0d07f9..bf4c70201 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx @@ -2,7 +2,7 @@ import { createMemo } from 'solid-js'; import cn from 'clsx'; import theme from 'panel/lib/theme'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { getQueryStatusLabel, getQueryStatusDetails, @@ -13,7 +13,7 @@ import { import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const StatusCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx index a57296731..d6563cde4 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx @@ -1,13 +1,13 @@ import cn from 'clsx'; import theme from 'panel/lib/theme'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { formatLogDate, formatLogTime } from 'panel/components/QueryLog/helpers'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const TimeCell = (props: Props) => ( diff --git a/client_v2/src/components/QueryLog/helpers.ts b/client_v2/src/components/QueryLog/helpers.ts index 927ba8579..3f0f3a2a9 100644 --- a/client_v2/src/components/QueryLog/helpers.ts +++ b/client_v2/src/components/QueryLog/helpers.ts @@ -16,9 +16,8 @@ import { getFilterNames, getServiceName, type Filter, - type Rule, } from 'panel/helpers/helpers'; -import { LogEntry, ResponseEntry, WhoisInfo } from './types'; +import { ResponseEntry, WhoisInfo } from './types'; const parseLogDate = (time: string): Date | null => { const parsedTime = new Date(time); @@ -140,13 +139,13 @@ const PROTOCOL_LABEL_GETTERS = { plain_dns: () => intl.getMessage('plain_dns'), } as const; -export const getStatusClassName = (reason: string): string => +export const getStatusClassName = (reason?: string): string => STATUS_COLOR_TO_CLASS[ FILTERED_STATUS_TO_COLOR_MAP[reason as keyof typeof FILTERED_STATUS_TO_COLOR_MAP] ] || ''; -export const isBlockedReason = (reason: string): boolean => - reason.startsWith('Filtered') && reason !== 'FilteredSafeSearch'; +export const isBlockedReason = (reason?: string): boolean => + !!reason && reason.startsWith('Filtered') && reason !== 'FilteredSafeSearch'; export const getProtocolName = (clientProto: string): string => { const key = SCHEME_TO_PROTOCOL_MAP[clientProto as keyof typeof SCHEME_TO_PROTOCOL_MAP]; @@ -185,18 +184,18 @@ export const getClientLocation = (whois?: WhoisInfo | null): string => [whois?.city, whois?.country].filter(Boolean).join(', '); type ResponseDetailsParams = { - elapsedMs: string; + elapsedMs?: string; filters: Filter[]; - reason: string; - rules: Rule[]; + reason?: string; + rules: { filter_list_id?: number; text?: string }[]; serviceName?: string; services?: { id: string; name: string }[]; whitelistFilters: Filter[]; }; export const getQueryStatusKey = ( - reason: string, - originalResponse: ResponseEntry[] = [], + reason?: string, + originalResponse: { value?: string; type?: string; ttl?: number }[] = [], ): Exclude => { switch (reason) { case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: @@ -216,7 +215,7 @@ export const getQueryStatusKey = ( return 'rewritten'; } - if (reason.startsWith('Filtered')) { + if (reason && reason.startsWith('Filtered')) { return 'blocked'; } @@ -225,8 +224,8 @@ export const getQueryStatusKey = ( }; export const getQueryReasonKey = ( - reason: string, - rules: Rule[] = [], + reason?: string, + rules: { filter_list_id?: number; text?: string }[] = [], ): Exclude => { switch (reason) { case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: @@ -286,7 +285,7 @@ export const getQueryReasonDetails = ({ }; export const filterLogsByStatus = < - T extends { reason: string; originalResponse?: ResponseEntry[] }, + T extends { reason?: string; originalResponse?: { type?: string; value?: string }[] }, >( logs: T[], status: QueryStatusKey | string, @@ -296,12 +295,12 @@ export const filterLogsByStatus = < } return logs.filter( - (log) => getQueryStatusKey(log.reason, log.originalResponse ?? []) === status, + (log) => getQueryStatusKey(log.reason ?? '', log.originalResponse ?? []) === status, ); }; export const hasPersistentClient = ( - entry: Pick, + entry: { client: string; client_id?: string; client_info?: { name?: string; ids?: string[] } | null }, persistentClientIds: string[], ): boolean => { const entryIds = [entry.client, entry.client_id, ...(entry.client_info?.ids ?? [])].filter( @@ -321,7 +320,7 @@ export const getResponseDetails = ({ whitelistFilters, }: ResponseDetailsParams): string => { const formattedElapsedMs = formatElapsedMs( - elapsedMs, + elapsedMs || '', intl.getMessage('milliseconds_abbreviation'), ); diff --git a/client_v2/src/components/QueryLog/types.ts b/client_v2/src/components/QueryLog/types.ts index ed9eb42df..dd37533c0 100644 --- a/client_v2/src/components/QueryLog/types.ts +++ b/client_v2/src/components/QueryLog/types.ts @@ -1,5 +1,7 @@ +import type { FilteringReason } from 'panel/api/model/filteringReason'; + export type ResponseEntry = { - value: string; + value?: string; type?: string; ttl?: number; }; @@ -47,7 +49,7 @@ export type LogEntry = { unicodeName?: string; type: string; response: ResponseEntry[]; - reason: string; + reason: FilteringReason; client: string; client_info: ClientInfo | null; tracker: TrackerInfo | null; diff --git a/client_v2/src/components/Settings/FiltersConfig.tsx b/client_v2/src/components/Settings/FiltersConfig.tsx index a71562d38..e83faf5ad 100644 --- a/client_v2/src/components/Settings/FiltersConfig.tsx +++ b/client_v2/src/components/Settings/FiltersConfig.tsx @@ -1,4 +1,4 @@ -import { createSignal, createEffect } from 'solid-js'; +import { createSignal, createEffect, untrack } from 'solid-js'; import intl from 'panel/common/intl'; import theme from 'panel/lib/theme'; @@ -21,7 +21,7 @@ export const FiltersConfig = (props: Props) => { const [enabled, setEnabled] = createSignal(props.initialValues.enabled); createEffect(() => { - const initial = props.initialValues; + const initial = untrack(() => props.initialValues); setFiltersConfig({ ...initial, enabled: enabled() }); }); diff --git a/client_v2/src/components/Settings/Settings.tsx b/client_v2/src/components/Settings/Settings.tsx index 9d81fcfbe..6a9988734 100644 --- a/client_v2/src/components/Settings/Settings.tsx +++ b/client_v2/src/components/Settings/Settings.tsx @@ -61,7 +61,7 @@ export const Settings = () => { const ss = safesearch(); if (!ss) return ''; const selected = Object.keys(SAFE_SEARCH_PROVIDERS) - .filter((key) => ss[key]) + .filter((key) => (ss as Record)[key]) .map(getSafeSearchProviderTitle); return selected.join(', '); }); @@ -214,7 +214,9 @@ export const Settings = () => { setSafesearchProvidersOpen(false)} - providers={settingsState.settingsList.safesearch} + providers={ + settingsState.settingsList.safesearch as Record + } enabled={safesearchEnabled()} processing={safesearchProcessing()} onSave={handleSafeSearchSave} diff --git a/client_v2/src/components/SetupGuide/MobileConfigForm.tsx b/client_v2/src/components/SetupGuide/MobileConfigForm.tsx index 71f014f1b..fc0046195 100644 --- a/client_v2/src/components/SetupGuide/MobileConfigForm.tsx +++ b/client_v2/src/components/SetupGuide/MobileConfigForm.tsx @@ -125,6 +125,7 @@ export const MobileConfigForm = (props: Props) => { onChange={handleHostChange} error={!!hostError()} errorMessage={hostError()} + size="large" />
@@ -138,6 +139,7 @@ export const MobileConfigForm = (props: Props) => { onChange={handlePortChange} error={!!portError()} errorMessage={portError()} + size="large" />
@@ -169,6 +171,7 @@ export const MobileConfigForm = (props: Props) => { onChange={handleClientIdChange} error={!!clientIdError()} errorMessage={clientIdError()} + size="large" />
diff --git a/client_v2/src/components/SetupGuide/SetupGuide.module.pcss b/client_v2/src/components/SetupGuide/SetupGuide.module.pcss index 0c96e2231..ea280e4bd 100644 --- a/client_v2/src/components/SetupGuide/SetupGuide.module.pcss +++ b/client_v2/src/components/SetupGuide/SetupGuide.module.pcss @@ -11,6 +11,30 @@ @media screen and (min-width: 1024px) { padding-top: 48px; } + + .guidePage { + padding-top: 16px; + padding-bottom: 24px; + margin: 0 auto; + } + + .pageTitle { + font-size: var(--fs-title-h4); + line-height: var(--lh-h4-normal); + + @media screen and (min-width: 1024px) { + font-size: var(--fs-title-h3); + line-height: var(--lh-h3-normal); + } + } + + .nav { + padding: 0 16px; + + @media screen and (min-width: 1024px) { + padding: 0; + } + } } .footer { @@ -21,61 +45,45 @@ } } -.stepRoot .guidePage { - padding-top: 16px; - padding-bottom: 24px; - margin: 0 auto; -} - -.stepRoot .pageTitle { - font-size: 24px; - - @media screen and (min-width: 1024px) { - font-size: 32px; - } -} - -.stepRoot .nav { - padding: 0 16px; - - @media screen and (min-width: 1024px) { - padding: 0; - } -} - .pageTitle { - font-size: 24px; - font-weight: 700; + font-size: var(--fs-title-h4); + line-height: var(--lh-h4-normal); + font-weight: var(--weight-bold); margin-bottom: 12px; @media screen and (min-width: 1024px) { - font-size: 32px; + font-size: var(--fs-title-h3); + line-height: var(--lh-h3-normal); } } .pageDesc { - font-size: 16px; + font-size: var(--fs-text-t2); + line-height: var(--lh-t2-normal); } .dnsTitle { - font-size: 20px; + font-size: var(--fs-title-h5); + line-height: var(--lh-h5-normal); + font-weight: var(--weight-bold); margin-bottom: 12px; margin-top: 40px; -} -@media screen and (min-width: 768px) { - .dnsTitle { - font-size: 24px; + @media screen and (min-width: 768px) { + font-size: var(--fs-title-h4); + line-height: var(--lh-h4-normal); } } .dnsSubtitle { margin: 24px 0 12px; - font-weight: 700; - font-size: 16px; + font-weight: var(--weight-bold); + font-size: var(--fs-text-t2); + line-height: var(--lh-t2-normal); @media screen and (min-width: 768px) { - font-size: 18px; + font-size: var(--fs-text-t1); + line-height: var(--lh-t1-normal); } } @@ -94,11 +102,13 @@ .guideTitle { margin-bottom: 24px; - font-size: 20px; - font-weight: 700; + font-size: var(--fs-title-h5); + line-height: var(--lh-h5-normal); + font-weight: var(--weight-bold); @media screen and (min-width: 768px) { - font-size: 24px; + font-size: var(--fs-title-h4); + line-height: var(--lh-h4-normal); } } @@ -110,12 +120,12 @@ list-style: none; padding: 0; margin: 12px 0 0 0; -} -.addressList li { - display: flex; - align-items: center; - gap: 8px; + li { + display: flex; + align-items: center; + gap: 8px; + } } .bulletIcon { @@ -129,8 +139,9 @@ .address { padding: 4px 12px; border-radius: 4px; - font-family: monospace; - font-size: 14px; + font-family: var(--font-family-monospace); + font-size: var(--fs-text-t3); + line-height: var(--lh-t3-normal); } .iosConfigSection { diff --git a/client_v2/src/components/Toasts/Toast.tsx b/client_v2/src/components/Toasts/Toast.tsx index 548d8cc20..a4bc65c0a 100644 --- a/client_v2/src/components/Toasts/Toast.tsx +++ b/client_v2/src/components/Toasts/Toast.tsx @@ -24,7 +24,7 @@ type ToastProps = { code?: string; }; -const Toast = (props: ToastProps) => { +export const Toast = (props: ToastProps) => { let timerRef: ReturnType | null = null; const removeCurrentToast = () => removeToast(props.id); @@ -89,7 +89,7 @@ const Toast = (props: ToastProps) => { >
@@ -115,5 +115,3 @@ const Toast = (props: ToastProps) => {
); }; - -export default Toast; diff --git a/client_v2/src/components/Toasts/index.tsx b/client_v2/src/components/Toasts/index.tsx index b61b9353e..df3ef88ae 100644 --- a/client_v2/src/components/Toasts/index.tsx +++ b/client_v2/src/components/Toasts/index.tsx @@ -1,15 +1,13 @@ import { For } from 'solid-js'; -import Toast from './Toast'; +import { Toast } from './Toast'; import './Toast.pcss'; import { toastsState } from 'panel/stores/toasts'; -const Toasts = () => { +export const Toasts = () => { return (
{(toast: any) => }
); }; - -export default Toasts; diff --git a/client_v2/src/components/UserRules/UserRules.tsx b/client_v2/src/components/UserRules/UserRules.tsx index ebfe6218b..e9cac58d1 100644 --- a/client_v2/src/components/UserRules/UserRules.tsx +++ b/client_v2/src/components/UserRules/UserRules.tsx @@ -22,7 +22,7 @@ import { RulesEditor } from './blocks/RulesEditor'; import { DNS_RECORD_TYPE_OPTIONS } from './types'; import { useUserRulesActions } from './useUserRulesActions'; -import type { CheckFormValues } from './types'; +import type { CheckFormValues, CheckResultData } from './types'; import s from './UserRules.module.pcss'; @@ -61,7 +61,7 @@ export const UserRules = () => { openDeleteRewriteModal, resetCurrentRewrite, } = useUserRulesActions({ - checkResult: () => filteringState.check, + checkResult: () => filteringState.check as CheckResultData, filteringEnabled: () => filteringState.enabled, settingsList: () => settingsState.settingsList, persistentClients: () => dashboardState.clients || [], @@ -83,7 +83,7 @@ export const UserRules = () => { }); createEffect(() => { - if (filteringState.check?.hostname) { + if ((filteringState.check as CheckResultData)?.hostname) { setIsResultVisible(true); } }); @@ -130,7 +130,10 @@ export const UserRules = () => { const showResultLoader = createMemo(() => isResultVisible() && isResultRefreshing()); const showResultCard = createMemo( - () => isResultVisible() && !isResultRefreshing() && Boolean(filteringState.check?.hostname), + () => + isResultVisible() && + !isResultRefreshing() && + Boolean((filteringState.check as CheckResultData)?.hostname), ); return ( @@ -182,7 +185,7 @@ export const UserRules = () => { setIsResultVisible(false)} onAction={handleAction} @@ -198,13 +201,17 @@ export const UserRules = () => { diff --git a/client_v2/src/components/UserRules/checkResultHelpers.tsx b/client_v2/src/components/UserRules/checkResultHelpers.tsx index 89882c9cf..fc7e5404b 100644 --- a/client_v2/src/components/UserRules/checkResultHelpers.tsx +++ b/client_v2/src/components/UserRules/checkResultHelpers.tsx @@ -1,6 +1,7 @@ import intl from 'panel/common/intl'; import { FILTERED_STATUS, SPECIAL_FILTER_ID } from 'panel/helpers/constants'; import { checkFiltered, getFilterName, type Filter } from 'panel/helpers/helpers'; +import type { FilteringReason } from 'panel/api/model/filteringReason'; import { CheckResultData, ResultAction, ResultActionKind } from './types'; @@ -244,7 +245,7 @@ export const getCheckResultMeta = ({ source: intl.getMessage('system_host_files'), }; default: { - const isFilteredReason = reason ? checkFiltered(reason) : false; + const isFilteredReason = reason ? checkFiltered(reason as FilteringReason) : false; return { tone: isFilteredReason ? 'blocked' : 'processed', diff --git a/client_v2/src/components/UserRules/helpers.ts b/client_v2/src/components/UserRules/helpers.ts index 11ec2c8f0..3bc01ec37 100644 --- a/client_v2/src/components/UserRules/helpers.ts +++ b/client_v2/src/components/UserRules/helpers.ts @@ -26,11 +26,11 @@ export const findPersistentClient = (clients: Client[], identifier?: string) => const normalizedIdentifier = normalizeClientIdentifier(identifier); const matches = clients.filter((client) => { - if (normalizeClientIdentifier(client.name) === normalizedIdentifier) { + if (normalizeClientIdentifier(client.name ?? '') === normalizedIdentifier) { return true; } - return client.ids.some( + return (client.ids ?? []).some( (clientId) => normalizeClientIdentifier(clientId) === normalizedIdentifier, ); }); diff --git a/client_v2/src/components/UserRules/types.ts b/client_v2/src/components/UserRules/types.ts index baf84fac7..1ab07e3e9 100644 --- a/client_v2/src/components/UserRules/types.ts +++ b/client_v2/src/components/UserRules/types.ts @@ -43,9 +43,9 @@ export type ResultAction = { }; export type RewriteEntry = { - domain: string; - answer: string; - enabled: boolean; + domain?: string; + answer?: string; + enabled?: boolean; }; export type RewriteDialogState = { diff --git a/client_v2/src/helpers/constants.ts b/client_v2/src/helpers/constants.ts index 17afc318d..81c0d2dec 100644 --- a/client_v2/src/helpers/constants.ts +++ b/client_v2/src/helpers/constants.ts @@ -1,3 +1,5 @@ +import type { DNSConfigBlockingMode, SafeSearchConfig } from 'panel/api/model'; + export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/; // matches hostname or *.wildcard @@ -5,9 +7,6 @@ export const R_HOST = /^(\*\.)?[\w.-]+$/; export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/; -export const R_IPV6 = - /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; - export const R_CIDR = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/; @@ -34,10 +33,6 @@ export const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/; export const R_HOSTNAME = /^[a-z0-9-]+$/; -export const R_IPV4_SUBNET = /^([0-9]|[1-2][0-9]|3[0-2])?$/; - -export const R_IPV6_SUBNET = /^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])?$/; - export const MIN_PASSWORD_LENGTH = 8; export const MAX_PASSWORD_LENGTH = 72; @@ -48,6 +43,8 @@ export const HTML_PAGES = { MAIN: '/', }; +export const LANGUAGE_QUERY_PARAM = 'lang'; + export const STATS_NAMES = { avg_processing_time: 'average_processing_time', blocked_filtering: 'Blocked by filters', @@ -57,13 +54,6 @@ export const STATS_NAMES = { replaced_safesearch: 'enforced_save_search', }; -export const STATUS_COLORS = { - blue: '#467fcf', - red: '#cd201f', - green: '#5eba00', - yellow: '#f1c40f', -}; - export const REPOSITORY = { URL: 'https://github.com/AdguardTeam/AdGuardHome', TRACKERS_DB: @@ -83,26 +73,16 @@ export const TERMS_LINK = export const UPSTREAM_CONFIGURATION_WIKI_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams'; -export const FILTERS_RELATIVE_LINK = '#filters'; - export const ADDRESS_IN_USE_TEXT = 'address already in use'; export const INSTALL_FIRST_STEP = 1; export const INSTALL_TOTAL_STEPS = 6; -export const SETTINGS_NAMES = { - filtering: 'filtering', - safebrowsing: 'safebrowsing', - parental: 'parental', - safesearch: 'safesearch', -}; - export const STANDARD_DNS_PORT = 53; export const STANDARD_WEB_PORT = 80; export const STANDARD_HTTPS_PORT = 443; export const DNS_OVER_TLS_PORT = 853; export const DNS_OVER_QUIC_PORT = 853; -export const MIN_PORT = 1; export const MAX_PORT = 65535; export const EMPTY_DATE = '0001-01-01T00:00:00Z'; @@ -110,9 +90,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z'; export const DEBOUNCE_TIMEOUT = 300; export const DEBOUNCE_FILTER_TIMEOUT = 500; export const CHECK_TIMEOUT = 1000; -export const HIDE_TOOLTIP_DELAY = 300; -export const SHOW_TOOLTIP_DELAY = 200; -export const MODAL_OPEN_TIMEOUT = 150; export const UNSAFE_PORTS = [ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103, @@ -157,11 +134,6 @@ export const TAB_TYPE = { MANUAL: 'manual', } as const; -export const CLIENT_ID = { - MAC: 'mac', - IP: 'ip', -}; - export const ENCRYPTION_SOURCE = { PATH: 'path', CONTENT: 'content', @@ -169,9 +141,6 @@ export const ENCRYPTION_SOURCE = { }; export const FILTERED = 'Filtered'; -export const NOT_FILTERED = 'NotFiltered'; - -export const DISABLED_STATS_INTERVAL = 0; export const HOUR = 60 * 60 * 1000; @@ -183,15 +152,7 @@ export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * export const RETENTION_CUSTOM = 1; -export const RETENTION_CUSTOM_INPUT = 'custom_retention_input'; - -export const CUSTOM_INTERVAL = 'customInterval'; - -export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; - -// Note that translation strings contain these modes (blocking_mode_CONSTANT) -// i.e. blocking_mode_default, blocking_mode_null_ip -export const BLOCKING_MODES = { +export const BLOCKING_MODES: { readonly [K in DNSConfigBlockingMode]: K } = { default: 'default', refused: 'refused', nxdomain: 'nxdomain', @@ -204,30 +165,13 @@ export const EDNS_MODES = { custom: 'custom', }; -// Note that translation strings contain these modes (theme_CONSTANT) -// i.e. theme_auto, theme_light. export const THEMES = { auto: 'auto', dark: 'dark', light: 'light', }; -export type SafeSearchProviderKey = - | 'google' - | 'youtube' - | 'bing' - | 'duckduckgo' - | 'yandex' - | 'pixabay'; - -export const SAFE_SEARCH_PROVIDER_KEYS: SafeSearchProviderKey[] = [ - 'google', - 'youtube', - 'bing', - 'duckduckgo', - 'yandex', - 'pixabay', -]; +export type SafeSearchProviderKey = Exclude; export const SAFE_SEARCH_PROVIDERS: Record = { google: 'Google', @@ -236,8 +180,13 @@ export const SAFE_SEARCH_PROVIDERS: Record = { duckduckgo: 'DuckDuckGo', yandex: 'Yandex', pixabay: 'Pixabay', + ecosia: 'Ecosia', }; +export const SAFE_SEARCH_PROVIDER_KEYS = Object.keys( + SAFE_SEARCH_PROVIDERS, +) as SafeSearchProviderKey[]; + export const WHOIS_ICONS = { location: 'location', orgname: 'network', @@ -251,12 +200,14 @@ export const DEFAULT_LOGS_FILTER = { reason: 'all', }; -export const DEFAULT_LANGUAGE = 'en'; +export type QueryLogFilter = { + search: string; + status: string; + reason: string; +}; export const QUERY_LOGS_PAGE_LIMIT = 20; -export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20; - export const FILTERED_STATUS = { FILTERED_BLACK_LIST: 'FilteredBlackList', NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList', @@ -344,10 +295,6 @@ export const QUERY_LOG_REASON_FILTER_QUERIES = Object.values(QUERY_LOG_REASON_FI {}, ); -export const RESPONSE_FILTER = QUERY_LOG_REASON_FILTER; - -export const RESPONSE_FILTER_QUERIES = QUERY_LOG_REASON_FILTER_QUERIES; - export const QUERY_STATUS_COLORS = { BLUE: 'blue', GREEN: 'green', @@ -431,38 +378,7 @@ export const DNS_REQUEST_OPTIONS = { PARALLEL: 'parallel', FASTEST_ADDR: 'fastest_addr', LOAD_BALANCING: 'load_balance', -}; - -export const DHCP_FORM_NAMES = { - DHCPv4: 'dhcpv4', - DHCPv6: 'dhcpv6', - DHCP_INTERFACES: 'dhcpInterfaces', -}; - -export const FORM_NAME = { - UPSTREAM: 'upstream', - DOMAIN_CHECK: 'domainCheck', - FILTER: 'filter', - REWRITES: 'rewrites', - LOGS_FILTER: 'logsFilter', - CLIENT: 'client', - LEASE: 'lease', - ACCESS: 'access', - BLOCKING_MODE: 'blockingMode', - ENCRYPTION: 'encryption', - FILTER_CONFIG: 'filterConfig', - LOG_CONFIG: 'logConfig', - SERVICES: 'services', - STATS_CONFIG: 'statsConfig', - INSTALL: 'install', - LOGIN: 'login', - CACHE: 'cache', - MOBILE_CONFIG: 'mobileConfig', - ...DHCP_FORM_NAMES, -}; - -export const SMALL_SCREEN_SIZE = 767; -export const MEDIUM_SCREEN_SIZE = 1024; +} as const; export const SECONDS_IN_DAY = 60 * 60 * 24; @@ -511,18 +427,18 @@ export const UPSTREAM_TIMEOUT = { export const DHCP_VALUES_PLACEHOLDERS = { ipv4: { + gateway_ip: '192.168.1.1', subnet_mask: '255.255.255.0', + range_start: '192.168.1.100', + range_end: '192.168.1.200', lease_duration: SECONDS_IN_DAY.toString(), }, ipv6: { range_start: '2001::1', - range_end: 'ff', lease_duration: SECONDS_IN_DAY.toString(), }, }; -export const TOAST_TRANSITION_TIMEOUT = 500; - export const TOAST_TYPES = { SUCCESS: 'success', ERROR: 'error', @@ -549,12 +465,6 @@ export const ADDRESS_TYPES = { UNKNOWN: 'UNKNOWN', }; -export const CACHE_CONFIG_FIELDS = { - cache_size: 'cache_size', - cache_ttl_min: 'cache_ttl_min', - cache_ttl_max: 'cache_ttl_max', -}; - export const COMMENT_LINE_DEFAULT_TOKEN = '#'; export const COMMENT_LINE_TOKENS = ['#', '!'] as const; export type CommentLineToken = (typeof COMMENT_LINE_TOKENS)[number]; @@ -573,14 +483,8 @@ export const DISABLE_PROTECTION_TIMINGS = { TOMORROW: 24 * 60 * 60 * 1000, }; -export const LOCAL_TIMEZONE_VALUE = 'Local'; - -export const TABLES_MIN_ROWS = 5; - export const MOBILE_TABLE_MAX_ROWS = 5; -export const DASHBOARD_TABLES_DEFAULT_PAGE_SIZE = 100; - export const TIME_UNITS = { HOURS: 'hours', DAYS: 'days', diff --git a/client_v2/src/helpers/form.tsx b/client_v2/src/helpers/form.tsx index 0b4073a94..dd436e10c 100644 --- a/client_v2/src/helpers/form.tsx +++ b/client_v2/src/helpers/form.tsx @@ -5,8 +5,10 @@ import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from ' * @param {string} ip * @returns {*} */ -export const ip4ToInt = (ip: any) => { - const intIp = ip.split('.').reduce((int: any, oct: any) => int * 256 + parseInt(oct, 10), 0); +export const ip4ToInt = (ip: string): number => { + const intIp = ip + .split('.') + .reduce((int: number, oct: string) => int * 256 + parseInt(oct, 10), 0); return Number.isNaN(intIp) ? 0 : intIp; }; @@ -14,20 +16,20 @@ export const ip4ToInt = (ip: any) => { * @param value {string} * @returns {*|number} */ -export const toNumber = (value: any) => value && parseInt(value, 10); +export const toNumber = (value: string): number | undefined => value && parseInt(value, 10); /** * @param value {string} * @returns {*|number} */ -export const toFloatNumber = (value: any) => value && parseFloat(value); +export const toFloatNumber = (value: string): number | undefined => value && parseFloat(value); /** * @param value {string} * @returns {boolean} */ -export const isValidAbsolutePath = (value: any) => +export const isValidAbsolutePath = (value: string): boolean => R_WIN_ABSOLUTE_PATH.test(value) || R_UNIX_ABSOLUTE_PATH.test(value); /** @@ -38,7 +40,7 @@ export const isValidAbsolutePath = (value: any) => * @example normalizeMac("AA-BB-CC-DD-EE-FF") // "AA:BB:CC:DD:EE:FF" * @example normalizeMac("aa:bb:cc:dd:ee:ff") // "AA:BB:CC:DD:EE:FF" */ -export const normalizeMac = (value: any) => { +export const normalizeMac = (value: string): string => { if (!value || typeof value !== 'string') return value; // Handle separator-less bare hex (12 or 16 chars) @@ -54,3 +56,18 @@ export const normalizeMac = (value: any) => { // Already colon-separated or other format — just uppercase return value.toUpperCase(); }; + +/** + * Trims whitespace, strips http(s):// prefix, trailing slash, and trailing + * dot (FQDN notation) from a server name. Does NOT strip port, path, query, + * or fragment — those are user mistakes left for validation to flag. + * + * @example normalizeServerName(" https://example.com/ ") // "example.com" + * @example normalizeServerName("example.com:443") // "example.com:443" (left) + */ +export const normalizeServerName = (value: string): string => + value + .trim() + .replace(/^https?:\/\//i, '') + .replace(/\.$/, '') + .replace(/\/$/, ''); diff --git a/client_v2/src/helpers/helpers.tsx b/client_v2/src/helpers/helpers.tsx index 10f7cc205..763385168 100644 --- a/client_v2/src/helpers/helpers.tsx +++ b/client_v2/src/helpers/helpers.tsx @@ -1,22 +1,19 @@ -import { parseISO, format as dateFormat } from 'date-fns'; -import round from 'lodash/round'; import ipaddr, { IPv4, IPv6 } from 'ipaddr.js'; import queryString from 'qs'; import intl from 'panel/common/intl'; import { getTrackerData } from './trackers/trackers'; +import type { TrackerData } from './trackers/trackers'; import { ADDRESS_TYPES, CHECK_TIMEOUT, COMMENT_LINE_DEFAULT_TOKEN, DEFAULT_DATE_FORMAT_OPTIONS, - DEFAULT_TIME_FORMAT, DETAILED_DATE_FORMAT_OPTIONS, DHCP_VALUES_PLACEHOLDERS, FILTERED, FILTERED_STATUS, R_CLIENT_ID, - STANDARD_DNS_PORT, STANDARD_HTTPS_PORT, STANDARD_WEB_PORT, SPECIAL_FILTER_ID, @@ -24,16 +21,51 @@ import { SHORT_DATE_FORMAT_OPTIONS, } from './constants'; import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from './localStorageHelper'; -import { DhcpInterfaces, InstallInterface } from '../initialState'; +import { LANGUAGES, BASE_LOCALE } from './twosky'; +import type { Lang } from 'panel/api/model/lang'; +import type { DnsAnswer } from 'panel/api/model/dnsAnswer'; +import type { ResultRule } from 'panel/api/model/resultRule'; +import type { FilteringReason } from 'panel/api/model/filteringReason'; +import type { FilterStatus } from 'panel/api/model/filterStatus'; +import type { TopArrayEntry } from 'panel/api/model/topArrayEntry'; +import type { ClientsFindEntry } from 'panel/api/model/clientsFindEntry'; +import type { ClientFindSubEntry } from 'panel/api/model/clientFindSubEntry'; +import type { QueryLogItemClient } from 'panel/api/model/queryLogItemClient'; +import type { QueryLogItemClientWhois } from 'panel/api/model/queryLogItemClientWhois'; +import type { QueryLogItemClientProto } from 'panel/api/model/queryLogItemClientProto'; +import type { QueryLogItem } from 'panel/api/model/queryLogItem'; -/** - * @param time {string} The time to format - * @param options {string} - * @returns {string} Returns the time in the format HH:mm:ss - */ -export const formatTime = (time: any, options = DEFAULT_TIME_FORMAT) => { - const parsedTime = parseISO(time); - return dateFormat(parsedTime, options); +export type NormalizedDnsResponse = { + value?: string; + type?: string; + ttl?: number; +}; + +export type NormalizedQueryLogItem = { + time: string; + domain: string; + unicodeName: string; + type: string; + response: NormalizedDnsResponse[]; + reason?: FilteringReason; + client: string; + client_proto?: QueryLogItemClientProto; + client_id?: string; + client_info: QueryLogItemClient | null; + filterId?: number; // @deprecated + rule?: string; // @deprecated + rules: ResultRule[]; + status?: string; + service_name?: string; + serviceName?: string; + originalAnswer?: DnsAnswer[]; + originalResponse: NormalizedDnsResponse[]; + tracker: TrackerData | null; + answer_dnssec?: boolean; + elapsedMs?: string; + upstream?: string; + cached?: boolean; + ecs?: string; }; /** @@ -68,8 +100,8 @@ export const formatDetailedDateTime = (dateTime: string) => export const formatShortDateTime = (dateTime: string) => formatDateTime(dateTime, SHORT_DATE_FORMAT_OPTIONS); -export const normalizeLogs = (logs: any) => - logs.map((log: any) => { +export const normalizeLogs = (logs: QueryLogItem[]): NormalizedQueryLogItem[] => + logs.map((log) => { const { answer, answer_dnssec, @@ -94,9 +126,9 @@ export const normalizeLogs = (logs: any) => const { name: domain, unicode_name: unicodeName, type } = question || {}; - const processResponse = (data: any) => + const processResponse = (data: DnsAnswer[] | undefined): NormalizedDnsResponse[] => Array.isArray(data) - ? data.map((response: any) => { + ? data.map((response: DnsAnswer) => { const { value, type, ttl } = response; return { @@ -152,28 +184,25 @@ export const normalizeLogs = (logs: any) => }; }); -// TODO (ik) type will fixed in query log task -export const normalizeHistory = (history: any) => - history.map((item: any, idx: number) => ({ - x: idx, - y: item, - })); - -export const normalizeTopStats = (stats: any) => - stats.map((item: any) => ({ +export const normalizeTopStats = (stats: TopArrayEntry[]): TopStat[] => + stats.map((item: TopArrayEntry) => ({ name: Object.keys(item)[0], - count: Object.values(item)[0], + count: Object.values(item)[0] as number, })); -export const addClientInfo = (data: any, clients: any, ...params: any[]) => - data.map((row: any) => { - let info = ''; +export const addClientInfo = ( + data: TopStat[], + clients: ClientsFindEntry[], + ...params: string[] +): (TopStat & { info: ClientFindSubEntry })[] => + data.map((row: TopStat) => { + let info: ClientFindSubEntry | null = null; params.find((param) => { - const id = row[param]; + const id = row[param as keyof TopStat]; if (id) { - const client = clients.find((item: any) => item[id]) || ''; - info = client?.[id] ?? ''; + const clientData = clients.find((item: ClientsFindEntry) => item[String(id)]); + info = clientData?.[String(id)] ?? null; } return info; @@ -181,13 +210,13 @@ export const addClientInfo = (data: any, clients: any, ...params: any[]) => return { ...row, - info, + info: info ?? {}, }; }); -export const normalizeFilters = (filters: any) => +export const normalizeFilters = (filters: FilterStatus['filters']) => filters - ? filters.map((filter: any) => { + ? filters.map((filter) => { const { id, url, @@ -208,7 +237,15 @@ export const normalizeFilters = (filters: any) => }) : []; -export const normalizeFilteringStatus = (filteringStatus: any) => { +export const normalizeFilteringStatus = ( + filteringStatus: FilterStatus, +): { + enabled: boolean | undefined; + userRules: string; + filters: Filter[]; + whitelistFilters: Filter[]; + interval: number | undefined; +} => { const { enabled, filters, @@ -227,21 +264,20 @@ export const normalizeFilteringStatus = (filteringStatus: any) => { }; }; -export const getPercent = (amount: any, number: any) => { - if (amount > 0 && number > 0) { - return round(100 / (amount / number), 2); - } - return 0; -}; - -export const captitalizeWords = (text: any) => +export const captitalizeWords = (text: string): string => text .split(/[ -_]/g) - .map((str: any) => str.charAt(0).toUpperCase() + str.substr(1)) + .map((str: string) => str.charAt(0).toUpperCase() + str.substr(1)) .join(' '); -export const getInterfaceIp = (option: any) => { - const addresses = (option?.ip_addresses ?? []).filter((ip: any) => typeof ip === 'string'); +type InterfaceWithIpAddresses = { ip_addresses?: string[] }; + +type TopStat = { name: string; count: number }; + +type ServiceEntry = { id: string; name: string }; + +export const getInterfaceIp = (option: InterfaceWithIpAddresses): string | undefined => { + const addresses = (option?.ip_addresses ?? []).filter((ip: string) => typeof ip === 'string'); const isIpv6 = (ip: string) => ip.includes(':'); const isIpv6LinkLocal = (ip: string) => ip.toLowerCase().startsWith('fe80:'); @@ -263,34 +299,6 @@ export const getInterfaceIp = (option: any) => { return ipv6NoZone || addresses[0]; }; -export const getIpList = (interfaces: InstallInterface[]) => - Object.values(interfaces) - .reduce( - (acc: string[], curr: InstallInterface) => acc.concat(curr.ip_addresses), - [] as string[], - ) - .sort(); - -/** - * @param {string} ip - * @param {number} [port] - * @returns {string} - */ -export const getDnsAddress = (ip: any, port = 0) => { - const isStandardDnsPort = port === STANDARD_DNS_PORT; - let address = ip; - - if (port) { - if (ip.includes(':') && !isStandardDnsPort) { - address = `[${ip}]:${port}`; - } else if (!isStandardDnsPort) { - address = `${ip}:${port}`; - } - } - - return address; -}; - const normalizeHost = (host: string) => { const isIpv6 = host.includes(':'); if (!isIpv6) { @@ -312,7 +320,7 @@ const normalizeHost = (host: string) => { * @param {number} [port] * @returns {string} */ -export const getWebAddress = (ip: any, port = 0) => { +export const getWebAddress = (ip: string, port: number = 0): string => { const isStandardWebPort = port === STANDARD_WEB_PORT; const rawHost = String(ip); @@ -322,7 +330,7 @@ export const getWebAddress = (ip: any, port = 0) => { return `http://${host}${portPart}`; }; -export const checkRedirect = (url: any, attempts: number = 1) => { +export const checkRedirect = (url: string, attempts: number = 1): boolean => { let count = attempts || 1; if (count > 10) { @@ -330,11 +338,13 @@ export const checkRedirect = (url: any, attempts: number = 1) => { return false; } - const rmTimeout = (t: any) => t && clearTimeout(t); - const setRecursiveTimeout = (time: any, ...args: any[]) => - setTimeout(checkRedirect, time, ...args); + const rmTimeout = (t: ReturnType | undefined) => t && clearTimeout(t); + const setRecursiveTimeout = ( + time: number, + ...args: [string, number] + ): ReturnType => setTimeout(checkRedirect, time, ...args); - let timeout: any; + let timeout: ReturnType | undefined; fetch(url) .then((response) => { @@ -353,7 +363,13 @@ export const checkRedirect = (url: any, attempts: number = 1) => { return false; }; -export const redirectToCurrentProtocol = (values: any, httpPort = 80) => { +type RedirectValues = { + enabled?: boolean; + force_https?: boolean; + port_https?: number; +}; + +export const redirectToCurrentProtocol = (values: RedirectValues, httpPort = 80) => { const { protocol, hostname, hash, port } = window.location; const { enabled, force_https, port_https } = values; const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : ''; @@ -376,36 +392,21 @@ export const redirectToCurrentProtocol = (values: any, httpPort = 80) => { * @param {string} text * @returns []string */ -export const splitByNewLine = (text: any) => { +export const splitByNewLine = (text: string | undefined | null): string[] => { if (!text) { return []; } - return text.split('\n').filter((n: any) => n.trim()); + return text.split('\n').filter((n: string) => n.trim()); }; -/** - * @param {string} text - * @returns {string} - */ -export const trimMultilineString = (text: any) => - splitByNewLine(text) - .map((line: any) => line.trim()) - .join('\n'); - -/** - * @param {string} text - * @returns {string} - */ -export const removeEmptyLines = (text: any) => splitByNewLine(text).join('\n'); - /** * @param {string} input * @returns {string} */ -export const trimLinesAndRemoveEmpty = (input: any) => +export const trimLinesAndRemoveEmpty = (input: string): string => input .split('\n') - .map((line: any) => line.trim()) + .map((line: string) => line.trim()) .filter(Boolean) .join('\n'); @@ -421,9 +422,14 @@ export const trimLinesAndRemoveEmpty = (input: any) => * @returns {Object.} normalizedTopClients.auto - auto clients * @returns {Object.} normalizedTopClients.configured - configured clients */ -export const normalizeTopClients = (topClients: any) => +export const normalizeTopClients = ( + topClients: (TopStat & { info: ClientFindSubEntry })[], +): { auto: Record; configured: Record } => topClients.reduce( - (acc: any, clientObj: any) => { + ( + acc: { auto: Record; configured: Record }, + clientObj: TopStat & { info: ClientFindSubEntry }, + ) => { const { name, count, @@ -439,35 +445,14 @@ export const normalizeTopClients = (topClients: any) => }, ); -export const sortClients = (clients: any) => { - const compare = (a: any, b: any) => { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); +export const msToSeconds = (milliseconds: number): number => Math.floor(milliseconds / 1000); - if (nameA > nameB) { - return 1; - } - if (nameA < nameB) { - return -1; - } +export const msToMinutes = (milliseconds: number): number => Math.floor(milliseconds / 1000 / 60); - return 0; - }; +export const msToHours = (milliseconds: number): number => + Math.floor(milliseconds / 1000 / 60 / 60); - return clients.sort(compare); -}; - -export const toggleAllServices = (services: any, change: any, isSelected: any) => { - services.forEach((service: any) => change(`blocked_services.${service.id}`, isSelected)); -}; - -export const msToSeconds = (milliseconds: any) => Math.floor(milliseconds / 1000); - -export const msToMinutes = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60); - -export const msToHours = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60); - -export const secondsToMilliseconds = (seconds: any) => { +export const secondsToMilliseconds = (seconds: number): number => { if (seconds) { return seconds * 1000; } @@ -475,12 +460,12 @@ export const secondsToMilliseconds = (seconds: any) => { return seconds; }; -export const msToDays = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60 / 24); - -export const normalizeRulesTextarea = (text: any) => +export const normalizeRulesTextarea = (text: string): string | undefined => text?.replace(/^\n/g, '').replace(/\n\s*\n/g, '\n'); -export const normalizeWhois = (whois: any) => { +export const normalizeWhois = ( + whois: QueryLogItemClientWhois, +): Partial & { location?: string } => { if (Object.keys(whois).length > 0) { const { city, country, ...values } = whois; let location = country || ''; @@ -507,7 +492,10 @@ export const normalizeWhois = (whois: any) => { }; }; -export const getPathWithQueryString = (path: any, params: any) => { +export const getPathWithQueryString = ( + path: string, + params: Record | undefined, +): string => { const searchParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { @@ -529,81 +517,28 @@ export const getPathWithQueryString = (path: any, params: any) => { return `${path}?${searchParams.toString()}`; }; -export const getParamsForClientsSearch = (data: any, param: any, additionalParam?: any) => { - const clients = new Set(); - data.forEach((e: any) => { - clients.add(e[param]); - if (e[additionalParam]) { - clients.add(e[additionalParam]); +export const getParamsForClientsSearch = ( + data: Record[], + param: string, + additionalParam?: string, +): { clients: { id: string }[] } => { + const clients = new Set(); + data.forEach((e: Record) => { + clients.add(e[param] as string | number); + if (e[additionalParam as string]) { + clients.add(e[additionalParam as string] as string | number); } }); return { - clients: Array.from(clients).map((id) => ({ id })), + clients: Array.from(clients).map((id) => ({ id: id as string })), }; }; -/** - * Creates onBlur handler that can normalize input if normalization function is specified - * - * @param {Object} event - * @param {Object} event.target - * @param {string} event.target.value - * @param {Object} input - * @param {function} input.onBlur - * @param {function} [normalizeOnBlur] - * @returns {function} - */ - -export const checkFiltered = (reason: any) => reason.indexOf(FILTERED) === 0; -export const checkRewrite = (reason: any) => reason === FILTERED_STATUS.REWRITE; -export const checkRewriteHosts = (reason: any) => reason === FILTERED_STATUS.REWRITE_HOSTS; -export const checkBlackList = (reason: any) => reason === FILTERED_STATUS.FILTERED_BLACK_LIST; -export const checkWhiteList = (reason: any) => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST; -// eslint-disable-next-line max-len -export const checkNotFilteredNotFound = (reason: any) => - reason === FILTERED_STATUS.NOT_FILTERED_NOT_FOUND; -export const checkSafeSearch = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH; -export const checkSafeBrowsing = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_BROWSING; -export const checkParental = (reason: any) => reason === FILTERED_STATUS.FILTERED_PARENTAL; -export const checkBlockedService = (reason: any) => +export const checkFiltered = (reason: FilteringReason): boolean => reason.indexOf(FILTERED) === 0; +export const checkBlockedService = (reason: FilteringReason): boolean => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE; -export const getCurrentFilter = (url: any, filters: any) => { - const filter = filters?.find((item: any) => url === item.url); - - if (filter) { - const { enabled, name, url } = filter; - return { - enabled, - name, - url, - }; - } - - return { - enabled: true, - name: '', - url: '', - }; -}; - -/** - * @param {object} initialValues - * @param {object} values - * @returns {object} Returns different values of objects - */ - -export const getObjDiff = (initialValues: any, values: any) => - Object.entries(values) - - .reduce((acc: any, [key, value]) => { - if (value !== initialValues[key]) { - acc[key] = value; - } - return acc; - }, {}); - /** * @param num {number} to format * @returns {string} Returns a string with a language-sensitive representation of this number @@ -640,24 +575,12 @@ export const formatCompactNumber = (num: number, decimals: number = 1): string = return sign + formatted + suffix; }; -/** - * @param arr {array} - * @param key {string} - * @param value {string} - * @returns {object} - */ -export const getMap = (arr: any, key: any, value: any) => - arr.reduce((acc: any, curr: any) => { - acc[curr[key]] = curr[value]; - return acc; - }, {}); - /** * @param parsedIp {object} ipaddr.js IPv4 or IPv6 object * @param parsedCidr {array} ipaddr.js CIDR array * @returns {boolean} */ -const isIpMatchCidr = (parsedIp: any, parsedCidr: any) => { +const isIpMatchCidr = (parsedIp: IPv4 | IPv6, parsedCidr: [IPv4 | IPv6, number]): boolean => { try { const cidrIpVersion = parsedCidr[0].kind(); const ipVersion = parsedIp.kind(); @@ -668,7 +591,7 @@ const isIpMatchCidr = (parsedIp: any, parsedCidr: any) => { } }; -export const isIpInCidr = (ip: any, cidr: any) => { +export const isIpInCidr = (ip: string, cidr: string): boolean => { try { const parsedIp = ipaddr.parse(ip); const parsedCidr = ipaddr.parseCIDR(cidr); @@ -698,7 +621,7 @@ export const isValidIpv6 = (value: string): boolean => { * @param {string} subnetMask * @returns {IPv4 | null} */ -export const parseSubnetMask = (subnetMask: any) => { +export const parseSubnetMask = (subnetMask: string): number | null => { try { return ipaddr.parse(subnetMask).prefixLengthFromSubnetMask(); } catch (e) { @@ -712,8 +635,10 @@ export const parseSubnetMask = (subnetMask: any) => { * @param {string} subnetMask * @returns {*} */ -export const subnetMaskToBitMask = (subnetMask: any) => - subnetMask.split('.').reduce((acc: any, cur: any) => acc - Math.log2(256 - Number(cur)), 32); +export const subnetMaskToBitMask = (subnetMask: string): number => + subnetMask + .split('.') + .reduce((acc: number, cur: string) => acc - Math.log2(256 - Number(cur)), 32); /** * @@ -721,7 +646,7 @@ export const subnetMaskToBitMask = (subnetMask: any) => * @returns {'IP' | 'CIDR' | 'CLIENT_ID' | 'UNKNOWN'} * */ -export const findAddressType = (address: any) => { +export const findAddressType = (address: string): string => { try { const cidrMaybe = address.includes('/'); @@ -745,9 +670,11 @@ export const findAddressType = (address: any) => { * @param ids {string[]} * @returns {Object} */ -export const separateIpsAndCidrs = (ids: any) => +export const separateIpsAndCidrs = ( + ids: string[], +): { ips: string[]; cidrs: string[]; clientIds: string[] } => ids.reduce( - (acc: any, curr: any) => { + (acc: { ips: string[]; cidrs: string[]; clientIds: string[] }, curr: string) => { const addressType = findAddressType(curr); if (addressType === ADDRESS_TYPES.IP) { @@ -764,25 +691,28 @@ export const separateIpsAndCidrs = (ids: any) => { ips: [], cidrs: [], clientIds: [] }, ); -export const countClientsStatistics = (ids: any, autoClients: any) => { +export const countClientsStatistics = ( + ids: string[], + autoClients: Record, +): number => { const { ips, cidrs, clientIds } = separateIpsAndCidrs(ids); - const ipsCount = ips.reduce((acc: any, curr: any) => { + const ipsCount = ips.reduce((acc: number, curr: string) => { const count = autoClients[curr] || 0; return acc + count; }, 0); - const clientIdsCount = clientIds.reduce((acc: any, curr: any) => { + const clientIdsCount = clientIds.reduce((acc: number, curr: string) => { const count = autoClients[curr] || 0; return acc + count; }, 0); - const cidrsCount = Object.entries(autoClients).reduce((acc: any, curr: any) => { + const cidrsCount = Object.entries(autoClients).reduce((acc: number, curr: [string, number]) => { const [id, count] = curr; if (!ipaddr.isValid(id)) { return acc; } - if (cidrs.some((cidr: any) => isIpInCidr(id, cidr))) { + if (cidrs.some((cidr: string) => isIpInCidr(id, cidr))) { // eslint-disable-next-line no-param-reassign acc += count; } @@ -810,10 +740,46 @@ export const formatElapsedMs = (elapsedMs: string, millisecondsLabel: string) => return `${formattedValue} ${millisecondsLabel}`; }; +/** + * Type guard: checks whether a string is a supported language code + * as defined by the {@link LANGUAGES} map from twosky. + */ +export const isLang = (value: string): value is Lang => value in LANGUAGES; + +/** + * Detects the best initial language from the browser or a previously-stored + * preference, validated against the supported {@link LANGUAGES} set. + * Falls back to {@code BASE_LOCALE} when no match is found. + */ +export const getBrowserLanguage = (): Lang => { + // 1. Previously saved language (localStorage) + const stored = LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE); + if (stored && isLang(stored)) { + return stored; + } + + // 2. Browser language + if (typeof navigator !== 'undefined' && navigator.language) { + const browserLang = navigator.language.toLowerCase(); + // Full locale first (e.g. "zh-tw") + if (isLang(browserLang)) { + return browserLang; + } + // Base language (e.g. "fr" from "fr-FR") + const base = browserLang.split('-')[0]; + if (base && isLang(base)) { + return base; + } + } + + // 3. Fallback + return BASE_LOCALE as Lang; +}; + /** * @param language {string} */ -export const setHtmlLangAttr = (language: any) => { +export const setHtmlLangAttr = (language: string): void => { window.document.documentElement.lang = language; }; @@ -822,7 +788,7 @@ export const setHtmlLangAttr = (language: any) => { * * @param {string} theme */ -export const setTheme = (theme: any) => { +export const setTheme = (theme: string): void => { LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.THEME, theme); }; @@ -832,14 +798,15 @@ export const setTheme = (theme: any) => { * @returns {string} */ -export const getTheme = () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.THEME) || THEMES.light; +export const getTheme = () => + LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.THEME) || THEMES.light; /** * Sets UI theme. * * @param theme */ -export const setUITheme = (theme: any) => { +export const setUITheme = (theme?: string): void => { let currentTheme = theme || getTheme(); if (currentTheme === THEMES.auto) { @@ -852,25 +819,6 @@ export const setUITheme = (theme: any) => { document.documentElement.style.colorScheme = currentTheme; }; -/** - * @param values {object} - * @returns {object} - */ - -export const replaceEmptyStringsWithZeroes = (values: any) => - Object.entries(values) - - .reduce((acc: any, [key, value]) => { - acc[key] = value === '' ? 0 : value; - return acc; - }, {}); - -/** - * @param value {number || string} - * @returns {string} - */ -export const replaceZeroWithEmptyString = (value: any) => (parseInt(value, 10) === 0 ? '' : value); - /** * @param {string} search * @param {string} status @@ -884,34 +832,11 @@ export const getLogsUrlParams = (search: string, status: string, reason: string) reason: reason || undefined, })}`; -export const processContent = (content: any) => - Array.isArray(content) - ? content.filter(([, value]) => value).reduce((acc, val) => acc.concat(val), []) - : content; - -// TODO check getObjectKeysSorted -type NestedObject = { - [key: string]: any; - order: number; -}; - -export const getObjectKeysSorted = < - T extends Record, - K extends keyof NestedObject, ->( - object: T, - sortKey: K, -): string[] => { - return Object.entries(object) - .sort(([, a], [, b]) => (a[sortKey] as number) - (b[sortKey] as number)) - .map(([key]) => key); -}; - /** * @param ip * @returns {[IPv4|IPv6, 33|129]} */ -const getParsedIpWithPrefixLength = (ip: any) => { +const getParsedIpWithPrefixLength = (ip: string): [IPv4 | IPv6, number] => { const MAX_PREFIX_LENGTH_V4 = 32; const MAX_PREFIX_LENGTH_V6 = 128; @@ -927,7 +852,7 @@ const getParsedIpWithPrefixLength = (ip: any) => { * @param item - ip or cidr * @returns {number[]} */ -const getAddressesComparisonBytes = (item: any) => { +const getAddressesComparisonBytes = (item: string): number[] => { // Sort ipv4 before ipv6 const IP_V4_COMPARISON_CODE = 0; const IP_V6_COMPARISON_CODE = 1; @@ -978,7 +903,7 @@ export const sortIp = (a: string, b: string): number => { * @param {number} filterId * @returns {string} */ -export const getSpecialFilterName = (filterId: any) => { +export const getSpecialFilterName = (filterId: number): string => { switch (filterId) { case SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES: return intl.getMessage('custom_rules'); @@ -1007,8 +932,8 @@ export type Filter = { }; export type Rule = { - filter_list_id: number; - text: string; + filter_list_id?: number; + text?: string; }; export const getFilterName = ( @@ -1029,64 +954,25 @@ export const getFilterName = ( }; export const getFilterNames = (rules: Rule[], filters: Filter[], whitelistFilters: Filter[]) => - rules.map(({ filter_list_id }: any) => - getFilterName(filters, whitelistFilters, filter_list_id), - ); - -export const getRuleNames = (rules: Rule[]) => rules.map(({ text }: Rule) => text); - -export const getFilterNameToRulesMap = ( - rules: Rule[], - filters: Filter[], - whitelistFilters: Filter[], -) => - rules.reduce((acc: any, { text, filter_list_id }: Rule) => { - const filterName = getFilterName(filters, whitelistFilters, filter_list_id); - - acc[filterName] = (acc[filterName] || []).concat(text); - return acc; - }, {}); - -export const getRulesToFilterList = ( - rules: Rule[], - filters: Filter[], - whitelistFilters: Filter[], - classes = { - list: 'filteringRules', - rule: 'filteringRules__rule font-monospace', - filter: 'filteringRules__filter', - }, -) => { - const filterNameToRulesMap: { string: string[] } = getFilterNameToRulesMap( - rules, - filters, - whitelistFilters, - ); - - return ( -
- {Object.entries(filterNameToRulesMap).reduce( - (acc: any, [filterName, rulesArr]) => - acc - .concat( - rulesArr.map((rule: any, _i: any) => ( -
{rule}
- )), - ) - .concat(
{filterName}
), - [], - )} -
- ); -}; + rules + .filter((r): r is Required => r.filter_list_id != null) + .map(({ filter_list_id }) => getFilterName(filters, whitelistFilters, filter_list_id)); /** - * @param ip {string} - * @param gateway_ip {string} - * @returns {{range_end: string, subnet_mask: string, range_start: string, - * lease_duration: string, gateway_ip: string}} + * @param {string[]} lines + * @returns {string[]} */ -export const calculateDhcpPlaceholdersIpv4 = (ip: string, gateway_ip: string) => { +export const filterOutComments = (lines: string[]): string[] => + lines.filter((line: string) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN)); + +/** + * Computes DHCP v4 placeholder values from the interface IP address. + * Replaces the last octet with 100 for range_start and 200 for range_end. + * @param ip - The interface's IPv4 address (e.g. "192.168.1.1") + * @param gatewayIp - The interface's gateway IP (falls back to `ip` if empty) + * @returns Pre-filled v4 config values + */ +export const calculateDhcpPlaceholdersIpv4 = (ip: string, gatewayIp: string) => { const LAST_OCTET_IDX = 3; const LAST_OCTET_RANGE_START = 100; const LAST_OCTET_RANGE_END = 200; @@ -1102,7 +988,7 @@ export const calculateDhcpPlaceholdersIpv4 = (ip: string, gateway_ip: string) => const { subnet_mask, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv4; return { - gateway_ip: gateway_ip || ip, + gateway_ip: gatewayIp || ip, subnet_mask, range_start, range_end, @@ -1110,90 +996,34 @@ export const calculateDhcpPlaceholdersIpv4 = (ip: string, gateway_ip: string) => }; }; +/** + * Computes DHCP v6 placeholder values (static defaults). + * @returns Pre-filled v6 config values + */ export const calculateDhcpPlaceholdersIpv6 = () => { - const { range_start, range_end, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv6; + const { range_start, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv6; return { range_start, - range_end, lease_duration, }; }; /** - * Add ip_addresses property - concatenated ipv4_addresses and ipv6_addresses for every interface - * @param interfaces - * @param interfaces.ipv4_addresses {string[]} - * @param interfaces.ipv6_addresses {string[]} - * @returns interfaces Interfaces enriched with ip_addresses property - */ - -export const enrichWithConcatenatedIpAddresses = (interfaces: DhcpInterfaces) => - Object.entries(interfaces) - - .reduce((acc: DhcpInterfaces, [k, v]) => { - const ipv4_addresses = v.ipv4_addresses ?? []; - const ipv6_addresses = v.ipv6_addresses ?? []; - - acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses); - return acc; - }, interfaces); - -export const isScrolledIntoView = (el: any) => { - const rect = el.getBoundingClientRect(); - const elemTop = rect.top; - const elemBottom = rect.bottom; - - return elemTop < window.innerHeight && elemBottom >= 0; -}; - -/** - * If this is a manually created client, return its name. - * If this is a "runtime" client, return it's IP address. - * @param clients {Array.} - * @param ip {string} + * @param {array} services + * @param {string} id * @returns {string} */ -export const getBlockingClientName = (clients: any, ip: any) => { - for (let i = 0; i < clients.length; i += 1) { - const client = clients[i]; - - if (client.ids.includes(ip)) { - return client.name; - } - } - return ip; -}; - -/** - * @param {string[]} lines - * @returns {string[]} - */ -export const filterOutComments = (lines: any) => - lines.filter((line: any) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN)); - -export const isCommentLine = (line: string) => /^\s*[#!]/.test(line); +export const getService = (services: ServiceEntry[], id: string): ServiceEntry | undefined => + services.find((s: ServiceEntry) => s.id === id); /** * @param {array} services * @param {string} id * @returns {string} */ -export const getService = (services: any, id: any) => services.find((s: any) => s.id === id); - -/** - * @param {array} services - * @param {string} id - * @returns {string} - */ -export const getServiceName = (services: any, id: any) => getService(services, id)?.name; - -/** - * @param {array} services - * @param {string} id - * @returns {string} - */ -export const getServiceIcon = (services: any, id: any) => getService(services, id)?.icon_svg; +export const getServiceName = (services: ServiceEntry[], id: string): string | undefined => + getService(services, id)?.name; /** * Decodes a base64-encoded SVG string. Returns an empty string on failure. diff --git a/client_v2/src/helpers/localStorageHelper.ts b/client_v2/src/helpers/localStorageHelper.ts index 0176ba922..8bfcaa118 100644 --- a/client_v2/src/helpers/localStorageHelper.ts +++ b/client_v2/src/helpers/localStorageHelper.ts @@ -9,29 +9,29 @@ export const LOCAL_STORAGE_KEYS = { }; export const LocalStorageHelper = { - setItem(key: any, value: any) { + setItem(key: string, value: unknown) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { - console.error(`Error setting ${key} in local storage: ${error.message}`); + console.error(`Error setting ${key} in local storage: ${(error as Error).message}`); } }, - getItem(key: any) { + getItem(key: string): T | null { try { const item = localStorage.getItem(key); - return item ? JSON.parse(item) : null; + return item ? (JSON.parse(item) as T) : null; } catch (error) { - console.error(`Error getting ${key} from local storage: ${error.message}`); + console.error(`Error getting ${key} from local storage: ${(error as Error).message}`); return null; } }, - removeItem(key: any) { + removeItem(key: string) { try { localStorage.removeItem(key); } catch (error) { - console.error(`Error removing ${key} from local storage: ${error.message}`); + console.error(`Error removing ${key} from local storage: ${(error as Error).message}`); } }, @@ -39,7 +39,7 @@ export const LocalStorageHelper = { try { localStorage.clear(); } catch (error) { - console.error(`Error clearing local storage: ${error.message}`); + console.error(`Error clearing local storage: ${(error as Error).message}`); } }, }; diff --git a/client_v2/src/helpers/renderFormattedClientCell.tsx b/client_v2/src/helpers/renderFormattedClientCell.tsx index f3194e7b1..c4ac8936b 100644 --- a/client_v2/src/helpers/renderFormattedClientCell.tsx +++ b/client_v2/src/helpers/renderFormattedClientCell.tsx @@ -1,23 +1,31 @@ import { Show } from 'solid-js'; +import type { JSXElement } from 'solid-js'; import { A } from '@solidjs/router'; import { normalizeWhois } from './helpers'; import { WHOIS_ICONS } from './constants'; +import type { QueryLogItemClientWhois } from 'panel/api/model/queryLogItemClientWhois'; -const getFormattedWhois = (whois: any) => { +type ClientCellInfo = { + name?: string; + whois_info?: QueryLogItemClientWhois; +}; + +const getFormattedWhois = (whois: QueryLogItemClientWhois) => { const whoisInfo = normalizeWhois(whois); - return Object.keys(whoisInfo).map((key) => { + return Object.entries(whoisInfo).map(([key, value]) => { const icon = WHOIS_ICONS[key as keyof typeof WHOIS_ICONS]; + const strValue = String(value ?? ''); return ( - +   - {whoisInfo[key]} + {strValue} ); }); @@ -33,13 +41,13 @@ const getFormattedWhois = (whois: any) => { * @returns {JSXElement} */ export const renderFormattedClientCell = ( - value: any, - info: any, + value: string, + info: ClientCellInfo | null, isDetailed = false, isLogs = false, ) => { - let whoisContainer = null; - let nameContainer: any = value; + let whoisContainer: JSXElement = null; + let nameContainer: JSXElement = value; if (info) { const { name, whois_info } = info; diff --git a/client_v2/src/helpers/trackers/trackers.ts b/client_v2/src/helpers/trackers/trackers.ts index aa11c80d1..1e1bea046 100644 --- a/client_v2/src/helpers/trackers/trackers.ts +++ b/client_v2/src/helpers/trackers/trackers.ts @@ -3,15 +3,15 @@ import whotracksmeWebsites from './whotracksme_web.json'; import trackersDb from './trackers.json'; import { REPOSITORY } from '../constants'; -/** - @typedef TrackerData - @type {object} - @property {string} id - tracker ID. - @property {string} name - tracker name. - @property {string} url - tracker website url. - @property {number} category - tracker category. - @property {source} source - tracker data source. - */ +/** Return type of {@link getTrackerData}. */ +export type TrackerData = { + id: string; + name: string; + url: string; + category: string; + source: number; + sourceData: { name: string; url: string } | null; +}; /** * Tracker data sources diff --git a/client_v2/src/helpers/twosky.ts b/client_v2/src/helpers/twosky.ts index 95a678888..1fba189ad 100644 --- a/client_v2/src/helpers/twosky.ts +++ b/client_v2/src/helpers/twosky.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/no-relative-packages import twosky from 'Twosky'; const homeV2 = twosky.find((p) => p.project_id === 'home_v2'); diff --git a/client_v2/src/helpers/useDebounce.ts b/client_v2/src/helpers/useDebounce.ts deleted file mode 100644 index 961c3f5e5..000000000 --- a/client_v2/src/helpers/useDebounce.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createSignal, createEffect, onCleanup } from 'solid-js'; - -const useDebounce = (value: any, delay: any) => { - const [debouncedValue, setDebouncedValue] = createSignal(value); - - createEffect(() => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - onCleanup(() => { - clearTimeout(handler); - }); - }); - - return [debouncedValue, setDebouncedValue]; -}; - -export default useDebounce; diff --git a/client_v2/src/helpers/version.ts b/client_v2/src/helpers/version.ts index 93106d390..9da3629e1 100644 --- a/client_v2/src/helpers/version.ts +++ b/client_v2/src/helpers/version.ts @@ -6,7 +6,7 @@ * @param right {string} - right version * @return {boolean} true if versions are equal */ -export const areEqualVersions = (left: any, right: any) => { +export const areEqualVersions = (left: string, right: string): boolean => { if (!left || !right) { return false; } diff --git a/client_v2/src/initialState.ts b/client_v2/src/initialState.ts index 95a91a4e3..79f220bc4 100644 --- a/client_v2/src/initialState.ts +++ b/client_v2/src/initialState.ts @@ -9,7 +9,28 @@ import { TIME_UNITS, } from './helpers/constants'; import { DEFAULT_BLOCKING_IPV4, DEFAULT_BLOCKING_IPV6 } from './stores/dnsConfig'; -import { Filter } from './helpers/helpers'; +import { Filter, type NormalizedQueryLogItem } from './helpers/helpers'; +import type { WhoisInfo } from './api/model/whoisInfo'; +import type { ClientAuto as AutoClient } from './api/model/clientAuto'; +import type { Client } from './api/model/client'; +import type { DHCPNetInterfaces } from './api/model/dHCPNetInterfaces'; +import type { TlsConfig } from './api/model/tlsConfig'; +import type { TlsConfigKeyType } from './api/model/tlsConfigKeyType'; +import type { DnsInfo200 } from './api/model/dnsInfo200'; +import type { DNSConfigBlockingMode, DNSConfigUpstreamMode } from './api/model'; +import type { FilterStatus } from './api/model/filterStatus'; +import type { DhcpStaticLease } from './api/model/dhcpStaticLease'; +import type { DhcpSearchResult } from './api/model/dhcpSearchResult'; +import type { Stats } from './api/model/stats'; +import type { GetStatsConfigResponse } from './api/model/getStatsConfigResponse'; +import type { GetQueryLogConfigResponse } from './api/model/getQueryLogConfigResponse'; +import type { RewriteEntry } from './api/model/rewriteEntry'; +import type { RewriteSettings } from './api/model/rewriteSettings'; +import type { BlockedServicesSchedule } from './api/model/blockedServicesSchedule'; +import type { BlockedService } from './api/model/blockedService'; +import type { ServiceGroup } from './api/model/serviceGroup'; +import type { QueryLogFilter } from './helpers/constants'; +import type { ToastNotice } from './stores/toasts'; export type InstallInterface = { flags: string; @@ -51,77 +72,29 @@ export type InstallData = { dnsVersion: string; }; -export type EncryptionData = { +export type EncryptionData = Partial< + Omit< + TlsConfig, + 'port_https' | 'port_dns_over_tls' | 'port_dns_over_quic' | 'port_dnscrypt' | 'dns_names' + > +> & { + // UI-only fields NOT in API model: processing: boolean; processingConfig: boolean; processingValidate: boolean; - enabled: boolean; - serve_plain_dns: boolean; - dns_names: any; - force_https: boolean; - issuer: string; - key_type: string; - not_after: string; - not_before: string; - port_dns_over_tls?: number; - port_dns_over_quic?: number; - port_https?: number; - port_dnscrypt?: number; - subject: string; - valid_chain: boolean; - valid_key: boolean; - valid_cert: boolean; - valid_pair: boolean; - status_cert: string; - status_key: string; - private_key: string; - server_name: string; - warning_validation: string; - certificate_chain: string; - certificate_path: string; - private_key_path: string; - private_key_saved: boolean; - allow_unencrypted_doh?: boolean; - dnscrypt_config_file?: string; + status_cert: string; // UI concatenation + status_key: string; // UI concatenation + allow_unencrypted_doh: boolean; + // Port fields: number from API, string from form input (initialized as ''): + port_https: number | string; + port_dns_over_tls: number | string; + port_dns_over_quic: number | string; + port_dnscrypt: number | string; + // Store initializes as null, API returns string[]: + dns_names: string[] | null; }; -export type Client = { - blocked_services: string[]; - blocked_services_schedule: { - sun?: { start: number; end: number }; - mon?: { start: number; end: number }; - tue?: { start: number; end: number }; - wed?: { start: number; end: number }; - thu?: { start: number; end: number }; - fri?: { start: number; end: number }; - sat?: { start: number; end: number }; - time_zone: string; - }; - filtering_enabled: boolean; - ids: string[]; - ignore_querylog: boolean; - ignore_statistics: boolean; - name: string; - parental_enabled: boolean; - safe_search: Record; - safebrowsing_enabled: boolean; - safesearch_enabled: boolean; - tags: string[]; - upstreams: string[]; - upstreams_cache_enabled: boolean; - upstreams_cache_size: number; - use_global_blocked_services: boolean; - use_global_settings: boolean; -}; - -export type WhoisInfo = Record; - -export type AutoClient = { - ip: string; - name: string; - source: string; - whois_info: WhoisInfo; -}; +export { type WhoisInfo, type AutoClient, type Client }; export type DashboardData = { processing: boolean; @@ -131,7 +104,7 @@ export type DashboardData = { processingUpdate: boolean; processingProfile: boolean; protectionEnabled: boolean; - protectionDisabledDuration: any; + protectionDisabledDuration: number | null; protectionCountdownActive: boolean; processingProtection: boolean; httpPort: number; @@ -166,7 +139,7 @@ export type SettingsData = { }; }; -export type RewritesData = { +export type RewritesData = RewriteSettings & { processing: boolean; processingAdd: boolean; processingDelete: boolean; @@ -174,17 +147,8 @@ export type RewritesData = { processingSettings: boolean; isModalOpen: boolean; modalType: string; - currentRewrite?: { - answer: string; - domain: string; - enabled: boolean; - }; - list: { - answer: string; - domain: string; - enabled: boolean; - }[]; - enabled: boolean; + currentRewrite?: RewriteEntry; + list: RewriteEntry[]; }; export type NormalizedTopClients = { @@ -192,37 +156,31 @@ export type NormalizedTopClients = { configured: Record; }; -export type StatsData = { - processingGetConfig: boolean; - processingSetConfig: boolean; - processingStats: boolean; - processingReset: boolean; - interval: number; - customInterval?: number; - dnsQueries: number[]; - blockedFiltering: number[]; - replacedParental: number[]; - replacedSafebrowsing: number[]; - topBlockedDomains: { name: string; count: number }[]; - topClients: { - name: string; - count: number; - info: any; - }[]; - normalizedTopClients?: NormalizedTopClients; - topQueriedDomains: { name: string; count: number }[]; - numBlockedFiltering: number; - numDnsQueries: number; - numReplacedParental: number; - numReplacedSafebrowsing: number; - numReplacedSafesearch: number; - avgProcessingTime: number; - timeUnits: string; - enabled: boolean; - topUpstreamsAvgTime: { name: string; count: number }[]; - topUpstreamsResponses: { name: string; count: number }[]; - ignored: string[]; -}; +export type StatsData = Omit< + Stats, + | 'top_queried_domains' + | 'top_clients' + | 'top_blocked_domains' + | 'top_upstreams_responses' + | 'top_upstreams_avg_time' + | 'time_units' +> & + Omit & { + processingGetConfig: boolean; + processingSetConfig: boolean; + processingStats: boolean; + processingReset: boolean; + interval: number; + customInterval?: number | null; + // Normalized top stats (from normalizeTopStats): + topBlockedDomains: { name: string; count: number }[]; + topClients: { name: string; count: number; info: string }[]; // info is string! + topQueriedDomains: { name: string; count: number }[]; + topUpstreamsAvgTime: { name: string; count: number }[]; + topUpstreamsResponses: { name: string; count: number }[]; + normalizedTopClients?: NormalizedTopClients; + timeUnits: string; + }; export type ClientsData = { processing: boolean; @@ -242,18 +200,6 @@ export type AccessData = { blocked_hosts: string; }; -export type DhcpInterface = { - name: string; - flags: string; - gateway_ip: string; - ip_addresses: string[]; - ipv4_addresses: string[]; - ipv6_addresses: string[]; - hardware_address: string; -}; - -export type DhcpInterfaces = Record; - export type DhcpData = { processing: boolean; processingStatus: boolean; @@ -265,16 +211,9 @@ export type DhcpData = { processingUpdating: boolean; enabled: boolean; interface_name: string; - check?: { - v4?: { - other_server?: { found: string; error?: string }; - static_ip?: { static: string; ip: string }; - }; - v6?: { - other_server?: { found: string; error?: string }; - static_ip?: { static: string; ip: string }; - }; - }; + // Use generated DhcpSearchResult: + check: DhcpSearchResult | null; + // Keep inline v4/v6 (required — always present after init): v4: { gateway_ip: string; subnet_mask: string; @@ -286,61 +225,43 @@ export type DhcpData = { range_start: string; lease_duration: number; }; - leases: { - hostname: string; - ip: string; - mac: string; - }[]; - staticLeases: { - hostname: string; - ip: string; - mac: string; - }[]; + // UI-normalized leases (flat without expires): + leases: { hostname: string; ip: string; mac: string }[]; + staticLeases: DhcpStaticLease[]; isModalOpen: boolean; - leaseModalConfig?: { - hostname: string; - ip: string; - mac: string; - }; + leaseModalConfig?: { hostname: string; ip: string; mac: string }; modalType: string; dhcp_available: boolean; - interfaces?: DhcpInterfaces; + interfaces?: DHCPNetInterfaces; }; -export type DnsConfigData = { +export type DnsConfigData = Omit< + DnsInfo200, + | 'upstream_dns' + | 'fallback_dns' + | 'bootstrap_dns' + | 'local_ptr_upstreams' + | 'ratelimit_whitelist' + | 'blocking_mode' + | 'upstream_mode' + | 'protection_enabled' + | 'protection_disabled_until' +> & { + // UI-only processing flags: processingGetConfig: boolean; processingSetConfig: boolean; - blocking_mode: string; - ratelimit: number; - blocking_ipv4: string; - blocking_ipv6: string; - blocked_response_ttl: number; - upstream_timeout: number; - edns_cs_enabled: boolean; - disable_ipv6: boolean; - dnssec_enabled: boolean; - upstream_dns_file: string; + // Normalized fields (string[] → newline-joined string): + blocking_mode: DNSConfigBlockingMode; + upstream_mode: DNSConfigUpstreamMode; upstream_dns: string; fallback_dns: string; bootstrap_dns: string; local_ptr_upstreams: string; ratelimit_whitelist: string; - upstream_mode: string; - resolve_clients: boolean; - use_private_ptr_resolvers: boolean; - default_local_ptr_upstreams: string[]; - ratelimit_subnet_len_ipv4?: number; - ratelimit_subnet_len_ipv6?: number; - edns_cs_use_custom?: boolean; - edns_cs_custom_ip?: string; - cache_size?: number; - cache_ttl_max?: number; - cache_ttl_min?: number; - cache_optimistic?: boolean; - cache_enabled?: boolean; }; -export type FilteringData = { +export type FilteringData = Omit & { + // UI-only fields: isModalOpen: boolean; processingFilters: boolean; processingRules: boolean; @@ -353,42 +274,37 @@ export type FilteringData = { isFilterAdded: boolean; isFilterRemoved: boolean; isFilterEdited: boolean; - filters: Filter[]; - whitelistFilters: any[]; - userRules: string; - interval: number; - enabled: boolean; modalType: string; modalFilterUrl: string; - check: any; + check: Record | Record; + // Normalized fields (camelCase from normalizeFilteringStatus): + filters: Filter[]; + whitelistFilters: Filter[]; // Note: whitelist (no underscore) — matches store + userRules: string; }; -export type QueryLogsData = { +export type QueryLogsData = Omit & { processingGetLogs: boolean; processingClear: boolean; processingGetConfig: boolean; processingSetConfig: boolean; processingAdditionalLogs: boolean; - interval: any; - logs: any[]; - enabled: boolean; + interval: number; + customInterval: number | null; + logs: NormalizedQueryLogItem[]; oldest: string; - filter: any; + filter: QueryLogFilter; isFiltered: boolean; - anonymize_client_ip: boolean; isDetailed: boolean; isEntireLog: boolean; - customInterval: any; - ignored: string[]; }; -export type ServicesData = { +export type ServicesData = BlockedServicesSchedule & { processing: boolean; processingAll: boolean; processingSet: boolean; - list: any; - allServices: any[]; - allGroups: any[]; + allServices: BlockedService[]; + allGroups: ServiceGroup[]; }; export type ModalsData = { @@ -413,6 +329,7 @@ export type ClientFormState = { duckduckgo: boolean; yandex: boolean; pixabay: boolean; + ecosia: boolean; }; ignore_querylog: boolean; ignore_statistics: boolean; @@ -453,6 +370,7 @@ export const getInitialClientFormState = (): ClientFormState => ({ duckduckgo: false, yandex: false, pixabay: false, + ecosia: false, }, ignore_querylog: false, ignore_statistics: false, @@ -482,14 +400,14 @@ export type RootState = { settings?: SettingsData; stats?: StatsData; install?: InstallData; - toasts: { notices: any[] }; + toasts: { notices: ToastNotice[] }; modals: ModalsData; clientForm: ClientFormState; }; export type InstallState = { install: InstallData; - toasts: { notices: any[] }; + toasts: { notices: ToastNotice[] }; }; export type LoginState = { @@ -499,7 +417,7 @@ export type LoginState = { password: string; error: unknown; }; - toasts: { notices: any[] }; + toasts: { notices: ToastNotice[] }; }; export const initialState: RootState = { @@ -608,7 +526,7 @@ export const initialState: RootState = { dns_names: null, force_https: false, issuer: '', - key_type: '', + key_type: '' as TlsConfigKeyType, not_after: '', not_before: '', subject: '', @@ -618,6 +536,7 @@ export const initialState: RootState = { valid_pair: false, status_cert: '', status_key: '', + allow_unencrypted_doh: false, certificate_chain: '', private_key: '', server_name: '', @@ -625,6 +544,10 @@ export const initialState: RootState = { certificate_path: '', private_key_path: '', private_key_saved: false, + port_https: '', + port_dns_over_tls: '', + port_dns_over_quic: '', + port_dnscrypt: '', }, filtering: { isModalOpen: false, @@ -681,10 +604,9 @@ export const initialState: RootState = { processing: true, processingAll: true, processingSet: false, - list: {}, allServices: [], allGroups: [], - }, + } as ServicesData, settings: { processing: true, processingTestUpstream: false, @@ -697,19 +619,19 @@ export const initialState: RootState = { processingReset: false, interval: DAY, customInterval: null, - dnsQueries: [], - blockedFiltering: [], - replacedParental: [], - replacedSafebrowsing: [], + dns_queries: [], + blocked_filtering: [], + replaced_parental: [], + replaced_safebrowsing: [], topBlockedDomains: [], topClients: [], topQueriedDomains: [], - numBlockedFiltering: 0, - numDnsQueries: 0, - numReplacedParental: 0, - numReplacedSafebrowsing: 0, - numReplacedSafesearch: 0, - avgProcessingTime: 0, + num_blocked_filtering: 0, + num_dns_queries: 0, + num_replaced_parental: 0, + num_replaced_safebrowsing: 0, + num_replaced_safesearch: 0, + avg_processing_time: 0, timeUnits: TIME_UNITS.HOURS, enabled: true, topUpstreamsAvgTime: [], diff --git a/client_v2/src/install/Setup/Auth.tsx b/client_v2/src/install/Setup/Auth.tsx index 66d6e7b20..a0e80360d 100644 --- a/client_v2/src/install/Setup/Auth.tsx +++ b/client_v2/src/install/Setup/Auth.tsx @@ -18,7 +18,14 @@ import { } from './helpers/helpers'; import styles from './styles.module.pcss'; -type AuthFormValues = { +const validateRequiredString = (value: string | undefined) => validateRequiredValue(value); +const validateRequiredBoolean = (value: boolean | undefined) => validateRequiredValue(value); +const validatePasswordLengthMsg = (value: string | undefined) => { + const result = validatePasswordLength(value); + return result === true ? intl.getMessage('password_requirements_characters') : result; +}; + +export type AuthFormValues = { username: string; password: string; confirm_password: string; @@ -123,15 +130,13 @@ export const Auth = (props: Props) => {
{(field, props) => ( {
{(field, props) => ( { {(field, props) => ( { + const languageOptions = () => + Object.entries(LANGUAGES).map(([value, label]) => ({ + value, + label, + })); -const Greeting = () => { const configureList = createMemo(() => [ intl.getMessage('setup_guide_greeting_list_1'), intl.getMessage('setup_guide_greeting_list_2'), @@ -14,11 +25,30 @@ const Greeting = () => { intl.getMessage('setup_guide_greeting_list_4'), intl.getMessage('setup_guide_greeting_list_5'), ]); + + const selectedLanguage = () => + languageOptions().find((opt) => opt.value === installState.language) || + languageOptions()[0]; + return (

{intl.getMessage('setup_guide_greeting_title')}

+ + +